2306 lines
		
	
	
		
			94 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
			
		
		
	
	
			2306 lines
		
	
	
		
			94 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
//
 | 
						|
//  LiveRoomViewModel.swift
 | 
						|
//  SodaLive
 | 
						|
//
 | 
						|
//  Created by klaus on 2023/08/14.
 | 
						|
//
 | 
						|
 | 
						|
import Foundation
 | 
						|
import Moya
 | 
						|
import Combine
 | 
						|
 | 
						|
import AgoraRtcKit
 | 
						|
import AgoraRtmKit
 | 
						|
 | 
						|
final class LiveRoomViewModel: NSObject, ObservableObject {
 | 
						|
    
 | 
						|
    private var agora: Agora = Agora.shared
 | 
						|
    
 | 
						|
    private let repository = LiveRepository()
 | 
						|
    private let userRepository = UserRepository()
 | 
						|
    private let reportRepository = ReportRepository()
 | 
						|
    private let rouletteRepository = RouletteRepository()
 | 
						|
    private let userActionRepository = UserActionRepository()
 | 
						|
    private var subscription = Set<AnyCancellable>()
 | 
						|
    
 | 
						|
    @Published var isSpeakerMute = false
 | 
						|
    @Published var isMute = false
 | 
						|
    @Published var role = LiveRoomMemberRole.LISTENER
 | 
						|
    
 | 
						|
    @Published var messageChangeFlag = false
 | 
						|
    @Published var messages = [LiveRoomChat]()
 | 
						|
    @Published var activeSpeakers = [UInt]()
 | 
						|
    @Published var muteSpeakers = [UInt]()
 | 
						|
    @Published var liveRoomInfo: GetRoomInfoResponse?
 | 
						|
    @Published var userProfile: GetLiveRoomUserProfileResponse?
 | 
						|
    
 | 
						|
    @Published var coverImageUrl: String?
 | 
						|
    
 | 
						|
    @Published var isLoadingLikeHeart = false
 | 
						|
    @Published var isLoading = false
 | 
						|
    @Published var errorMessage = ""
 | 
						|
    @Published var reportMessage = ""
 | 
						|
    @Published var isShowReportPopup = false
 | 
						|
    @Published var isShowErrorPopup = false
 | 
						|
    @Published var isShowUserProfilePopup = false
 | 
						|
    @Published var changeIsAdult = false {
 | 
						|
        didSet {
 | 
						|
            if changeIsAdult && !UserDefaults.bool(forKey: .auth) {
 | 
						|
                agora.speakerMute(true)
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    @Published var popupContent = ""
 | 
						|
    @Published var popupCancelTitle: String? = nil
 | 
						|
    @Published var popupCancelAction: (() -> Void)? = nil
 | 
						|
    @Published var popupConfirmTitle: String? = nil
 | 
						|
    @Published var popupConfirmAction: (() -> Void)? = nil
 | 
						|
    @Published var isShowPopup = false {
 | 
						|
        didSet {
 | 
						|
            if !isShowPopup {
 | 
						|
                resetPopupContent()
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    @Published var isShowProfileList = false
 | 
						|
    @Published var isShowProfilePopup = false {
 | 
						|
        didSet {
 | 
						|
            if !isShowProfilePopup {
 | 
						|
                selectedProfile = nil
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    @Published var selectedProfile: LiveRoomMember?
 | 
						|
    
 | 
						|
    @Published var isShowNotice = false {
 | 
						|
        didSet {
 | 
						|
            if isShowNotice {
 | 
						|
                isShowMenuPan = false
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    @Published var isShowMenuPan = false {
 | 
						|
        didSet {
 | 
						|
            if isShowMenuPan {
 | 
						|
                isShowNotice = false
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    @Published var isShowDonationPopup = false
 | 
						|
    
 | 
						|
    @Published var isShowDonationMessagePopup = false
 | 
						|
    
 | 
						|
    @Published var isShowDonationRankingPopup = false
 | 
						|
    
 | 
						|
    @Published var isShowHeartRankingPopup = false
 | 
						|
    
 | 
						|
    @Published var isSpeakerFold = false
 | 
						|
    
 | 
						|
    @Published var isShowQuitPopup = false
 | 
						|
    
 | 
						|
    @Published var isShowLiveEndPopup = false
 | 
						|
    
 | 
						|
    @Published var isShowEditRoomInfoDialog = false
 | 
						|
    
 | 
						|
    @Published var isShowShareView = false
 | 
						|
    
 | 
						|
    @Published var shareMessage = ""
 | 
						|
    
 | 
						|
    @Published var isShowKickOutPopup = false
 | 
						|
    @Published var kickOutDesc = ""
 | 
						|
    @Published var kickOutId = 0 {
 | 
						|
        didSet {
 | 
						|
            kickOutDesc = "\(getUserNicknameAndProfileUrl(accountId: kickOutId).nickname)님을 내보내시겠어요?"
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    @Published var totalDonationCan = 0
 | 
						|
    @Published var totalHeartCount = 0
 | 
						|
    @Published var donationMessageList = [LiveRoomDonationMessage]()
 | 
						|
    @Published var donationMessageCount = 0
 | 
						|
    
 | 
						|
    @Published var isShowingNewChat = false
 | 
						|
    @Published var isShowPhotoPicker = false
 | 
						|
    @Published var noticeViewWidth: CGFloat = UIFont.systemFontSize
 | 
						|
    @Published var noticeViewHeight: CGFloat = UIFont.systemFontSize
 | 
						|
    
 | 
						|
    @Published var isBgOn = true
 | 
						|
    @Published var isSignatureOn = true
 | 
						|
    @Published var isEntryMessageEnabled = true
 | 
						|
    @Published var donationStatus: GetLiveRoomDonationStatusResponse?
 | 
						|
    @Published var heartStatus: GetLiveRoomHeartListResponse?
 | 
						|
    
 | 
						|
    @Published private(set) var offset: CGFloat = 0
 | 
						|
    @Published private(set) var originOffset: CGFloat = 0
 | 
						|
    private var isCheckedOriginOffset: Bool = false
 | 
						|
    
 | 
						|
    @Published var coverImage: UIImage? = nil
 | 
						|
    
 | 
						|
    @Published var isShowReportMenu = false
 | 
						|
    @Published var isShowUesrBlockConfirm = false
 | 
						|
    @Published var isShowUesrReportView = false
 | 
						|
    @Published var isShowProfileReportConfirm = false
 | 
						|
    @Published var isShowNoChattingConfirm = false
 | 
						|
    @Published var isShowNoticeLikeHeart = false {
 | 
						|
        didSet {
 | 
						|
            if !isShowNoticeLikeHeart {
 | 
						|
                isAvailableLikeHeart = true
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    @Published var reportUserId = 0
 | 
						|
    @Published var reportUserNickname = ""
 | 
						|
    @Published var reportUserIsBlocked = false
 | 
						|
    
 | 
						|
    @Published var noChattingUserId = 0
 | 
						|
    @Published var noChattingUserNickname = ""
 | 
						|
    @Published var noChattingUserProfileUrl = ""
 | 
						|
    
 | 
						|
    private let noChattingTime = 180
 | 
						|
    @Published var isNoChatting = false
 | 
						|
    @Published var remainingNoChattingTime = 0
 | 
						|
    
 | 
						|
    @Published var isActiveRoulette = false
 | 
						|
    
 | 
						|
    @Published var isShowRouletteSettings = false
 | 
						|
    
 | 
						|
    @Published var isShowRoulettePreview = false
 | 
						|
    @Published var roulettePreviewList = [RoulettePreview]()
 | 
						|
    
 | 
						|
    @Published var isShowRoulette = false
 | 
						|
    @Published var rouletteItems = [String]()
 | 
						|
    @Published var rouletteSelectedItem = ""
 | 
						|
    var rouletteCan = 0
 | 
						|
    
 | 
						|
    @Published var signatureImageUrl = "" {
 | 
						|
        didSet {
 | 
						|
            showSignatureImage()
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    @Published var signature: LiveRoomDonationResponse? = nil {
 | 
						|
        didSet {
 | 
						|
            showSignatureImage()
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    @Published var heartNickname: String? = nil {
 | 
						|
        didSet {
 | 
						|
            if heartNickname != nil {
 | 
						|
                showNextHeartMessage()
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    @Published var heartNicknameList = [String]()
 | 
						|
    
 | 
						|
    private var menuId = 0
 | 
						|
    @Published var menu = ""
 | 
						|
    @Published var menuList = [GetMenuPresetResponse]()
 | 
						|
    
 | 
						|
    @Published var isActivateMenu = false {
 | 
						|
        didSet {
 | 
						|
            if !isActivateMenu {
 | 
						|
                menu = ""
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    @Published var selectedMenu: SelectedMenu? = nil
 | 
						|
    
 | 
						|
    @Published var hearts: [Heart] = []
 | 
						|
    
 | 
						|
    var signatureImageUrls = [String]()
 | 
						|
    var signatureList = [LiveRoomDonationResponse]()
 | 
						|
    var isShowSignatureImage = false
 | 
						|
    
 | 
						|
    var timer: DispatchSourceTimer?
 | 
						|
    var heartTimer: DispatchSourceTimer?
 | 
						|
    var periodicPlaybackTimer: DispatchSourceTimer?
 | 
						|
    
 | 
						|
    var isAvailableLikeHeart = false
 | 
						|
    
 | 
						|
    private var blockedMemberIdList = Set<Int>()
 | 
						|
    
 | 
						|
    private var hasInvokedJoinChannel = false
 | 
						|
    
 | 
						|
    func getBlockedMemberIdList() {
 | 
						|
        userRepository.getBlockedMemberIdList()
 | 
						|
            .sink { result in
 | 
						|
                switch result {
 | 
						|
                case .finished:
 | 
						|
                    DEBUG_LOG("finish")
 | 
						|
                case .failure(let error):
 | 
						|
                    ERROR_LOG(error.localizedDescription)
 | 
						|
                }
 | 
						|
            } receiveValue: { [unowned self] response in
 | 
						|
                let responseData = response.data
 | 
						|
                
 | 
						|
                do {
 | 
						|
                    let jsonDecoder = JSONDecoder()
 | 
						|
                    let decoded = try jsonDecoder.decode(ApiResponse<[Int]>.self, from: responseData)
 | 
						|
                    
 | 
						|
                    if let data = decoded.data, decoded.success {
 | 
						|
                        self.blockedMemberIdList.removeAll()
 | 
						|
                        self.blockedMemberIdList.formUnion(data)
 | 
						|
                    }
 | 
						|
                } catch {
 | 
						|
                }
 | 
						|
            }
 | 
						|
            .store(in: &subscription)
 | 
						|
    }
 | 
						|
    
 | 
						|
    func setOriginOffset(_ offset: CGFloat) {
 | 
						|
        guard !isCheckedOriginOffset else { return }
 | 
						|
        self.originOffset = offset
 | 
						|
        self.offset = offset
 | 
						|
        isCheckedOriginOffset = true
 | 
						|
    }
 | 
						|
    
 | 
						|
    func setOffset(_ offset: CGFloat) {
 | 
						|
        guard isCheckedOriginOffset else { return }
 | 
						|
        self.offset = offset
 | 
						|
    }
 | 
						|
    
 | 
						|
    func initAgoraEngine() {
 | 
						|
        agora.rtcEngineDelegate = self
 | 
						|
        agora.rtmClientDelegate = self
 | 
						|
        agora.initialize()
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func deInitAgoraEngine() {
 | 
						|
        agora.deInit()
 | 
						|
    }
 | 
						|
    
 | 
						|
    func agoraConnectSuccess(isManager: Bool) {
 | 
						|
        self.isLoading = false
 | 
						|
        if isManager {
 | 
						|
            role = .SPEAKER
 | 
						|
        } else {
 | 
						|
            role = .LISTENER
 | 
						|
        }
 | 
						|
        
 | 
						|
        DEBUG_LOG("agoraConnectSuccess")
 | 
						|
        
 | 
						|
        if containNoChatRoom() {
 | 
						|
            startNoChatting()
 | 
						|
        }
 | 
						|
        
 | 
						|
        startPeriodicPlaybackValidation()
 | 
						|
    }
 | 
						|
    
 | 
						|
    func agoraConnectFail() {
 | 
						|
        self.isLoading = false
 | 
						|
        DEBUG_LOG("agoraConnectFail")
 | 
						|
        AppState.shared.roomId = 0
 | 
						|
        AppState.shared.isShowPlayer = false
 | 
						|
    }
 | 
						|
    
 | 
						|
    func quitRoom() {
 | 
						|
        isLoading = true
 | 
						|
        
 | 
						|
        if let index = muteSpeakers.firstIndex(of: UInt(UserDefaults.int(forKey: .userId))) {
 | 
						|
            muteSpeakers.remove(at: index)
 | 
						|
        }
 | 
						|
        
 | 
						|
        repository.quitRoom(roomId: AppState.shared.roomId)
 | 
						|
            .sink { result in
 | 
						|
                switch result {
 | 
						|
                case .finished:
 | 
						|
                    DEBUG_LOG("finish")
 | 
						|
                case .failure(let error):
 | 
						|
                    ERROR_LOG(error.localizedDescription)
 | 
						|
                }
 | 
						|
            } receiveValue: { [unowned self] response in
 | 
						|
                let responseData = response.data
 | 
						|
                
 | 
						|
                do {
 | 
						|
                    let jsonDecoder = JSONDecoder()
 | 
						|
                    let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
 | 
						|
                    
 | 
						|
                    if decoded.success {
 | 
						|
                        self.deInitAgoraEngine()
 | 
						|
                        self.liveRoomInfo = nil
 | 
						|
                        AppState.shared.roomId = 0
 | 
						|
                    } else {
 | 
						|
                        if let message = decoded.message {
 | 
						|
                            self.errorMessage = message
 | 
						|
                        } else {
 | 
						|
                            self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
						|
                        }
 | 
						|
                        
 | 
						|
                        self.isShowErrorPopup = true
 | 
						|
                    }
 | 
						|
                } catch {
 | 
						|
                    self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
						|
                    self.isShowErrorPopup = true
 | 
						|
                }
 | 
						|
                
 | 
						|
                self.isLoading = false
 | 
						|
            }
 | 
						|
            .store(in: &subscription)
 | 
						|
    }
 | 
						|
    
 | 
						|
    func getRoomInfo(userId: Int = 0, onSuccess: @escaping (String) -> Void = { _ in }) {
 | 
						|
        isLoading = true
 | 
						|
        
 | 
						|
        repository.getRoomInfo(roomId: AppState.shared.roomId)
 | 
						|
            .sink { result in
 | 
						|
                switch result {
 | 
						|
                case .finished:
 | 
						|
                    DEBUG_LOG("finish")
 | 
						|
                case .failure(let error):
 | 
						|
                    ERROR_LOG(error.localizedDescription)
 | 
						|
                }
 | 
						|
            } receiveValue: { [unowned self] response in
 | 
						|
                let responseData = response.data
 | 
						|
                
 | 
						|
                do {
 | 
						|
                    let jsonDecoder = JSONDecoder()
 | 
						|
                    let decoded = try jsonDecoder.decode(ApiResponse<GetRoomInfoResponse>.self, from: responseData)
 | 
						|
                    
 | 
						|
                    if let data = decoded.data, decoded.success {
 | 
						|
                        self.liveRoomInfo = data
 | 
						|
                        
 | 
						|
                        if self.coverImageUrl != data.coverImageUrl {
 | 
						|
                            self.coverImageUrl = data.coverImageUrl
 | 
						|
                        }
 | 
						|
                        
 | 
						|
                        self.isActiveRoulette = data.isActiveRoulette
 | 
						|
                        self.isLoading = true
 | 
						|
                        
 | 
						|
                        let rtcState = self.agora.getRtcConnectionState()
 | 
						|
                        let rtcConnected = rtcState == AgoraConnectionState.connected
 | 
						|
                        let rtmLoggedIn = self.agora.isRtmLoggedIn()
 | 
						|
                        
 | 
						|
                        if (!hasInvokedJoinChannel && !(rtcConnected && rtmLoggedIn)) {
 | 
						|
                            hasInvokedJoinChannel = true
 | 
						|
                            self.agora.joinRtcChannel(rtcToken: data.rtcToken, channelName: data.channelName)
 | 
						|
                            self.agora.rtmLogin(
 | 
						|
                                creatorId: data.creatorId,
 | 
						|
                                rtmToken: data.rtmToken,
 | 
						|
                                channelName: data.channelName,
 | 
						|
                                onConnectSuccess: self.agoraConnectSuccess,
 | 
						|
                                onConnectFail: self.agoraConnectFail
 | 
						|
                            )
 | 
						|
                        } else {
 | 
						|
                            DEBUG_LOG("joinChannel - skip (rtcConnected=\(rtcConnected), rtmLoggedIn=\(rtmLoggedIn), hasInvokedJoinChannel=\(hasInvokedJoinChannel))")
 | 
						|
                        }
 | 
						|
                        
 | 
						|
                        getTotalDonationCan()
 | 
						|
                        getTotalHeartCount()
 | 
						|
                        
 | 
						|
                        if data.isAdult && !UserDefaults.bool(forKey: .auth) {
 | 
						|
                            changeIsAdult = true
 | 
						|
                        }
 | 
						|
                        
 | 
						|
                        if (userId > 0 && data.creatorId == UserDefaults.int(forKey: .userId)) {
 | 
						|
                            let nickname = getUserNicknameAndProfileUrl(accountId: userId).nickname
 | 
						|
                            onSuccess(nickname)
 | 
						|
                        }
 | 
						|
                    } else {
 | 
						|
                        if let message = decoded.message {
 | 
						|
                            self.errorMessage = message
 | 
						|
                        } else {
 | 
						|
                            self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
						|
                        }
 | 
						|
                        
 | 
						|
                        self.isShowErrorPopup = true
 | 
						|
                    }
 | 
						|
                    
 | 
						|
                    self.isLoading = false
 | 
						|
                } catch {
 | 
						|
                    self.isLoading = false
 | 
						|
                    self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
						|
                    self.isShowErrorPopup = true
 | 
						|
                }
 | 
						|
            }
 | 
						|
            .store(in: &subscription)
 | 
						|
    }
 | 
						|
    
 | 
						|
    func toggleMute() {
 | 
						|
        isMute.toggle()
 | 
						|
        agora.mute(isMute)
 | 
						|
        
 | 
						|
        if isMute {
 | 
						|
            muteSpeakers.append(UInt(UserDefaults.int(forKey: .userId)))
 | 
						|
        } else {
 | 
						|
            if let index = muteSpeakers.firstIndex(of: UInt(UserDefaults.int(forKey: .userId))) {
 | 
						|
                muteSpeakers.remove(at: index)
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    func toggleSpeakerMute() {
 | 
						|
        isSpeakerMute.toggle()
 | 
						|
        agora.speakerMute(isSpeakerMute)
 | 
						|
    }
 | 
						|
    
 | 
						|
    func sendMessage(chatMessage: String, onSuccess: @escaping () -> Void) {
 | 
						|
        DispatchQueue.main.async {[unowned self] in
 | 
						|
            if isNoChatting {
 | 
						|
                self.popupContent = "\(remainingNoChattingTime)초 동안 채팅하실 수 없습니다"
 | 
						|
                self.isShowPopup = true
 | 
						|
            } else if chatMessage.count > 0 {
 | 
						|
                agora.sendMessageToGroup(textMessage: chatMessage) { _, error in
 | 
						|
                    if error == nil {
 | 
						|
                        let (nickname, profileUrl) = self.getUserNicknameAndProfileUrl(accountId: UserDefaults.int(forKey: .userId))
 | 
						|
                        let rank = self.getUserRank(userId: UserDefaults.int(forKey: .userId))
 | 
						|
                        self.messages.append(LiveRoomNormalChat(userId: UserDefaults.int(forKey: .userId), profileUrl: profileUrl, nickname: nickname, rank: rank, chat: chatMessage))
 | 
						|
                        
 | 
						|
                        self.invalidateChat()
 | 
						|
                    }
 | 
						|
                    
 | 
						|
                    onSuccess()
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    func donation(can: Int, message: String = "", isSecret: Bool = false) {
 | 
						|
        if isSecret && can < 10 {
 | 
						|
            popupContent = "비밀 미션은 최소 10캔 이상부터 이용이 가능합니다."
 | 
						|
            isShowPopup = true
 | 
						|
        } else if can < 1 {
 | 
						|
            popupContent = "1캔 이상 후원하실 수 있습니다."
 | 
						|
            isShowPopup = true
 | 
						|
        } else {
 | 
						|
            isLoading = true
 | 
						|
            
 | 
						|
            repository.donation(roomId: AppState.shared.roomId, can: can, message: message, isSecret: isSecret)
 | 
						|
                .sink { result in
 | 
						|
                    switch result {
 | 
						|
                    case .finished:
 | 
						|
                        DEBUG_LOG("finish")
 | 
						|
                    case .failure(let error):
 | 
						|
                        ERROR_LOG(error.localizedDescription)
 | 
						|
                    }
 | 
						|
                } receiveValue: { [unowned self] response in
 | 
						|
                    let responseData = response.data
 | 
						|
                    
 | 
						|
                    do {
 | 
						|
                        let jsonDecoder = JSONDecoder()
 | 
						|
                        let decoded = try jsonDecoder.decode(ApiResponse<LiveRoomDonationResponse>.self, from: responseData)
 | 
						|
                        
 | 
						|
                        self.isLoading = false
 | 
						|
                        
 | 
						|
                        if decoded.success {
 | 
						|
                            var rawMessage = ""
 | 
						|
                            
 | 
						|
                            if isSecret {
 | 
						|
                                rawMessage = "\(can)캔으로 비밀미션을 보냈습니다.🤫"
 | 
						|
                            } else {
 | 
						|
                                rawMessage = "\(can)캔을 후원하셨습니다.💰🪙"
 | 
						|
                            }
 | 
						|
                            
 | 
						|
                            let donationRawMessage = LiveRoomChatRawMessage(
 | 
						|
                                type: isSecret ? .SECRET_DONATION : .DONATION,
 | 
						|
                                message: rawMessage,
 | 
						|
                                can: can,
 | 
						|
                                signature: decoded.data,
 | 
						|
                                signatureImageUrl: decoded.data?.imageUrl,
 | 
						|
                                donationMessage: message
 | 
						|
                            )
 | 
						|
                            
 | 
						|
                            UserDefaults.set(UserDefaults.int(forKey: .can) - can, forKey: .can)
 | 
						|
                            
 | 
						|
                            if isSecret {
 | 
						|
                                agora.sendRawMessageToPeer(peerId: String(liveRoomInfo!.creatorId), rawMessage: donationRawMessage) { [unowned self] _, error in
 | 
						|
                                    if error == nil {
 | 
						|
                                        let (nickname, profileUrl) = self.getUserNicknameAndProfileUrl(accountId: UserDefaults.int(forKey: .userId))
 | 
						|
                                        self.messages.append(
 | 
						|
                                            LiveRoomDonationChat(
 | 
						|
                                                memberId: UserDefaults.int(forKey: .userId),
 | 
						|
                                                profileUrl: profileUrl,
 | 
						|
                                                nickname: nickname,
 | 
						|
                                                chat: rawMessage,
 | 
						|
                                                can: can,
 | 
						|
                                                donationMessage: message
 | 
						|
                                            )
 | 
						|
                                        )
 | 
						|
                                        
 | 
						|
                                        addSignature(signature: decoded.data)
 | 
						|
                                        
 | 
						|
                                        self.invalidateChat()
 | 
						|
                                    } else {
 | 
						|
                                        refundDonation()
 | 
						|
                                    }
 | 
						|
                                } fail: { [unowned self] in
 | 
						|
                                    refundDonation()
 | 
						|
                                }
 | 
						|
                            } else {
 | 
						|
                                agora.sendRawMessageToGroup(rawMessage: donationRawMessage) { [unowned self] _, error in
 | 
						|
                                    if error == nil {
 | 
						|
                                        let (nickname, profileUrl) = self.getUserNicknameAndProfileUrl(accountId: UserDefaults.int(forKey: .userId))
 | 
						|
                                        self.messages.append(
 | 
						|
                                            LiveRoomDonationChat(
 | 
						|
                                                memberId: UserDefaults.int(forKey: .userId),
 | 
						|
                                                profileUrl: profileUrl,
 | 
						|
                                                nickname: nickname,
 | 
						|
                                                chat: rawMessage,
 | 
						|
                                                can: can,
 | 
						|
                                                donationMessage: message
 | 
						|
                                            )
 | 
						|
                                        )
 | 
						|
                                        
 | 
						|
                                        totalDonationCan += can
 | 
						|
                                        addSignature(signature: decoded.data)
 | 
						|
                                        
 | 
						|
                                        self.invalidateChat()
 | 
						|
                                    } else {
 | 
						|
                                        refundDonation()
 | 
						|
                                    }
 | 
						|
                                } fail: { [unowned self] in
 | 
						|
                                    refundDonation()
 | 
						|
                                }
 | 
						|
                            }
 | 
						|
                        } else {
 | 
						|
                            if let message = decoded.message {
 | 
						|
                                self.popupContent = message
 | 
						|
                            } else {
 | 
						|
                                self.popupContent = "후원에 실패했습니다.\n다시 후원해주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
						|
                            }
 | 
						|
                            
 | 
						|
                            self.isShowPopup = true
 | 
						|
                        }
 | 
						|
                    } catch {
 | 
						|
                        self.isLoading = false
 | 
						|
                        self.popupContent = "후원에 실패했습니다.\n다시 후원해주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
						|
                        self.isShowPopup = true
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                .store(in: &subscription)
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func refundDonation() {
 | 
						|
        isLoading = true
 | 
						|
        
 | 
						|
        repository.refundDonation(roomId: AppState.shared.roomId)
 | 
						|
            .sink { result in
 | 
						|
                switch result {
 | 
						|
                case .finished:
 | 
						|
                    DEBUG_LOG("finish")
 | 
						|
                case .failure(let error):
 | 
						|
                    ERROR_LOG(error.localizedDescription)
 | 
						|
                }
 | 
						|
            } receiveValue: { [unowned self] response in
 | 
						|
                let responseData = response.data
 | 
						|
                
 | 
						|
                do {
 | 
						|
                    let jsonDecoder = JSONDecoder()
 | 
						|
                    let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
 | 
						|
                    
 | 
						|
                    self.isLoading = false
 | 
						|
                    
 | 
						|
                    if decoded.success {
 | 
						|
                        self.popupContent = "후원에 실패했습니다.\n다시 후원해주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
						|
                        self.isShowPopup = true
 | 
						|
                    } else {
 | 
						|
                        if let message = decoded.message {
 | 
						|
                            self.errorMessage = message
 | 
						|
                        } else {
 | 
						|
                            self.popupContent = "후원에 실패한 캔이 환불되지 않았습니다\n고객센터로 문의해주세요."
 | 
						|
                        }
 | 
						|
                        
 | 
						|
                        self.isShowPopup = true
 | 
						|
                    }
 | 
						|
                } catch {
 | 
						|
                    self.isLoading = false
 | 
						|
                    self.popupContent = "후원에 실패한 캔이 환불되지 않았습니다\n고객센터로 문의해주세요."
 | 
						|
                    self.isShowPopup = true
 | 
						|
                }
 | 
						|
            }
 | 
						|
            .store(in: &subscription)
 | 
						|
    }
 | 
						|
    
 | 
						|
    func inviteSpeaker(peerId: Int) {
 | 
						|
        agora.sendMessageToPeer(peerId: String(peerId), rawMessage: LiveRoomRequestType.INVITE_SPEAKER.rawValue.data(using: .utf8)!) { [unowned self] _, error in
 | 
						|
            if error == nil {
 | 
						|
                self.popupContent = "스피커 요청을 보냈습니다.\n잠시만 기다려 주세요."
 | 
						|
                self.isShowPopup = true
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    func changeListener(peerId: Int, isFromManager: Bool = false) {
 | 
						|
        agora.sendMessageToPeer(peerId: String(peerId), rawMessage: LiveRoomRequestType.CHANGE_LISTENER.rawValue.data(using: .utf8)!) { [unowned self] _, error in
 | 
						|
            if error == nil {
 | 
						|
                if isFromManager {
 | 
						|
                    getRoomInfo()
 | 
						|
                    setManagerMessage()
 | 
						|
                    releaseManagerMessageToPeer(userId: peerId)
 | 
						|
                    self.popupContent = "\(getUserNicknameAndProfileUrl(accountId: peerId).nickname)님을 스탭에서 해제했어요."
 | 
						|
                } else {
 | 
						|
                    self.popupContent = "\(getUserNicknameAndProfileUrl(accountId: peerId).nickname)님을 리스너로 변경했어요."
 | 
						|
                }
 | 
						|
                
 | 
						|
                self.isShowPopup = true
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func getUserNicknameAndProfileUrl(accountId: Int) -> (nickname: String, profileUrl: String) {
 | 
						|
        for staff in liveRoomInfo!.managerList {
 | 
						|
            if staff.id == accountId {
 | 
						|
                return (staff.nickname, staff.profileImage)
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        for speaker in liveRoomInfo!.speakerList {
 | 
						|
            if speaker.id == accountId {
 | 
						|
                return (speaker.nickname, speaker.profileImage)
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        for listener in liveRoomInfo!.listenerList {
 | 
						|
            if listener.id == accountId {
 | 
						|
                return (listener.nickname, listener.profileImage)
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        return ("", "")
 | 
						|
    }
 | 
						|
    
 | 
						|
    func isEqualToStaffId(creatorId: Int) -> Bool {
 | 
						|
        for staff in liveRoomInfo!.managerList {
 | 
						|
            if staff.id == creatorId {
 | 
						|
                return true
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        return false
 | 
						|
    }
 | 
						|
    
 | 
						|
    func setListener() {
 | 
						|
        repository.setListener(roomId: AppState.shared.roomId, userId: UserDefaults.int(forKey: .userId))
 | 
						|
            .sink { result in
 | 
						|
                switch result {
 | 
						|
                case .finished:
 | 
						|
                    DEBUG_LOG("finish")
 | 
						|
                case .failure(let error):
 | 
						|
                    ERROR_LOG(error.localizedDescription)
 | 
						|
                }
 | 
						|
            } receiveValue: { [unowned self] response in
 | 
						|
                let responseData = response.data
 | 
						|
                
 | 
						|
                do {
 | 
						|
                    let jsonDecoder = JSONDecoder()
 | 
						|
                    let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
 | 
						|
                    
 | 
						|
                    if decoded.success {
 | 
						|
                        self.role = .LISTENER
 | 
						|
                        self.agora.setRole(role: .audience)
 | 
						|
                        self.isMute = false
 | 
						|
                        self.agora.mute(isMute)
 | 
						|
                        if let index = self.muteSpeakers.firstIndex(of: UInt(UserDefaults.int(forKey: .userId))) {
 | 
						|
                            self.muteSpeakers.remove(at: index)
 | 
						|
                        }
 | 
						|
                        self.getRoomInfo()
 | 
						|
                    }
 | 
						|
                } catch {
 | 
						|
                }
 | 
						|
            }
 | 
						|
            .store(in: &subscription)
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func setSpeaker() {
 | 
						|
        repository.setSpeaker(roomId: AppState.shared.roomId, userId: UserDefaults.int(forKey: .userId))
 | 
						|
            .sink { result in
 | 
						|
                switch result {
 | 
						|
                case .finished:
 | 
						|
                    DEBUG_LOG("finish")
 | 
						|
                case .failure(let error):
 | 
						|
                    ERROR_LOG(error.localizedDescription)
 | 
						|
                }
 | 
						|
            } receiveValue: { [unowned self] response in
 | 
						|
                let responseData = response.data
 | 
						|
                
 | 
						|
                do {
 | 
						|
                    let jsonDecoder = JSONDecoder()
 | 
						|
                    let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
 | 
						|
                    
 | 
						|
                    if decoded.success {
 | 
						|
                        self.role = .SPEAKER
 | 
						|
                        self.agora.setRole(role: .broadcaster)
 | 
						|
                        self.popupContent = "스피커가 되었어요!"
 | 
						|
                        self.isShowPopup = true
 | 
						|
                        self.isMute = false
 | 
						|
                        self.getRoomInfo()
 | 
						|
                    }
 | 
						|
                } catch {
 | 
						|
                }
 | 
						|
            }
 | 
						|
            .store(in: &subscription)
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func resetPopupContent() {
 | 
						|
        errorMessage = ""
 | 
						|
        popupContent = ""
 | 
						|
        popupCancelTitle = nil
 | 
						|
        popupCancelAction = nil
 | 
						|
        popupConfirmTitle = nil
 | 
						|
        popupConfirmAction = nil
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func requestSpeakerAllow(_ peerId: String) {
 | 
						|
        agora.sendMessageToPeer(peerId: peerId, rawMessage: LiveRoomRequestType.REQUEST_SPEAKER_ALLOW.rawValue.data(using: .utf8)!, completion: nil)
 | 
						|
    }
 | 
						|
    
 | 
						|
    func editLiveRoomInfo(title: String, notice: String, isAdult: Bool) {
 | 
						|
        let request = EditLiveRoomInfoRequest(
 | 
						|
            title: liveRoomInfo!.title != title ? title : nil,
 | 
						|
            notice: liveRoomInfo!.notice != notice ? notice : nil,
 | 
						|
            numberOfPeople: nil,
 | 
						|
            beginDateTimeString: nil,
 | 
						|
            timezone: nil,
 | 
						|
            menuPanId: isActivateMenu ? menuId : 0,
 | 
						|
            menuPan: isActivateMenu ? menu : "",
 | 
						|
            isActiveMenuPan: isActivateMenu,
 | 
						|
            isAdult: liveRoomInfo!.isAdult != isAdult ? isAdult : nil
 | 
						|
        )
 | 
						|
        
 | 
						|
        if (request.title == nil && request.notice == nil && coverImage == nil && menu == liveRoomInfo?.menuPan && request.isAdult == nil) {
 | 
						|
            return
 | 
						|
        }
 | 
						|
        
 | 
						|
        var multipartData = [MultipartFormData]()
 | 
						|
        
 | 
						|
        let encoder = JSONEncoder()
 | 
						|
        encoder.outputFormatting = .withoutEscapingSlashes
 | 
						|
        
 | 
						|
        if (request.title != nil || request.notice != nil || request.isAdult != nil || menu != liveRoomInfo?.menuPan) {
 | 
						|
            let jsonData = try? encoder.encode(request)
 | 
						|
            if let jsonData = jsonData {
 | 
						|
                multipartData.append(MultipartFormData(provider: .data(jsonData), name: "request"))
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        if let coverImage = coverImage, let imageData = coverImage.jpegData(compressionQuality: 0.8) {
 | 
						|
            multipartData.append(
 | 
						|
                MultipartFormData(
 | 
						|
                    provider: .data(imageData),
 | 
						|
                    name: "coverImage",
 | 
						|
                    fileName: "\(UUID().uuidString)_\(Date().timeIntervalSince1970 * 1000).jpg",
 | 
						|
                    mimeType: "image/*")
 | 
						|
            )
 | 
						|
        }
 | 
						|
        
 | 
						|
        repository.editLiveRoomInfo(roomId: AppState.shared.roomId, parameters: multipartData)
 | 
						|
            .sink { result in
 | 
						|
                switch result {
 | 
						|
                case .finished:
 | 
						|
                    DEBUG_LOG("finish")
 | 
						|
                case .failure(let error):
 | 
						|
                    ERROR_LOG(error.localizedDescription)
 | 
						|
                }
 | 
						|
            } receiveValue: { [unowned self] response in
 | 
						|
                let responseData = response.data
 | 
						|
                
 | 
						|
                do {
 | 
						|
                    let jsonDecoder = JSONDecoder()
 | 
						|
                    let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
 | 
						|
                    
 | 
						|
                    if decoded.success {
 | 
						|
                        self.coverImage = nil
 | 
						|
                        self.menuList.removeAll()
 | 
						|
                        self.isActivateMenu = false
 | 
						|
                        self.selectedMenu = nil
 | 
						|
                        self.menu = ""
 | 
						|
                        self.isShowMenuPan = false
 | 
						|
                        self.getRoomInfo()
 | 
						|
                        
 | 
						|
                        let editRoomInfoMessage = LiveRoomChatRawMessage(
 | 
						|
                            type: .EDIT_ROOM_INFO,
 | 
						|
                            message: "",
 | 
						|
                            can: 0,
 | 
						|
                            donationMessage: ""
 | 
						|
                        )
 | 
						|
                        
 | 
						|
                        self.agora.sendRawMessageToGroup(rawMessage: editRoomInfoMessage)
 | 
						|
                        self.errorMessage = "라이브 정보가 수정되었습니다."
 | 
						|
                        self.isShowErrorPopup = true
 | 
						|
                    } else {
 | 
						|
                        self.errorMessage = decoded.message ?? "라이브 정보를 수정하지 못했습니다.\n다시 시도해 주세요."
 | 
						|
                        self.isShowErrorPopup = true
 | 
						|
                    }
 | 
						|
                } catch {
 | 
						|
                    self.errorMessage = "라이브 정보를 수정하지 못했습니다.\n다시 시도해 주세요."
 | 
						|
                    self.isShowErrorPopup = true
 | 
						|
                }
 | 
						|
            }
 | 
						|
            .store(in: &subscription)
 | 
						|
    }
 | 
						|
    
 | 
						|
    func selectMenuPreset(selectedMenuPreset: SelectedMenu) {
 | 
						|
        if menuList.isEmpty && (selectedMenuPreset == .MENU_2 || selectedMenuPreset == .MENU_3) {
 | 
						|
            errorMessage = "메뉴 1을 먼저 설정하세요"
 | 
						|
            isShowPopup = true
 | 
						|
            return
 | 
						|
        }
 | 
						|
        
 | 
						|
        if menuList.count == 1 && selectedMenuPreset == .MENU_3 {
 | 
						|
            errorMessage = "메뉴 1과 메뉴 2를 먼저 설정하세요"
 | 
						|
            isShowPopup = true
 | 
						|
            return
 | 
						|
        }
 | 
						|
        
 | 
						|
        if self.selectedMenu != selectedMenuPreset {
 | 
						|
            self.selectedMenu = selectedMenuPreset
 | 
						|
            
 | 
						|
            if menuList.count > selectedMenuPreset.rawValue {
 | 
						|
                let menu = menuList[selectedMenuPreset.rawValue]
 | 
						|
                self.menu = menu.menu
 | 
						|
                self.menuId = menu.id
 | 
						|
            } else {
 | 
						|
                self.menu = ""
 | 
						|
                self.menuId = 0
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    func getAllMenuPreset(onFailure: @escaping () -> Void) {
 | 
						|
        isLoading = true
 | 
						|
        
 | 
						|
        repository.getAllMenuPreset(creatorId: UserDefaults.int(forKey: .userId))
 | 
						|
            .sink { result in
 | 
						|
                switch result {
 | 
						|
                case .finished:
 | 
						|
                    DEBUG_LOG("finish")
 | 
						|
                case .failure(let error):
 | 
						|
                    ERROR_LOG(error.localizedDescription)
 | 
						|
                }
 | 
						|
            } receiveValue: { [unowned self] response in
 | 
						|
                self.isLoading = false
 | 
						|
                let responseData = response.data
 | 
						|
                
 | 
						|
                do {
 | 
						|
                    let jsonDecoder = JSONDecoder()
 | 
						|
                    let decoded = try jsonDecoder.decode(ApiResponse<[GetMenuPresetResponse]>.self, from: responseData)
 | 
						|
                    
 | 
						|
                    if let data = decoded.data, decoded.success {
 | 
						|
                        self.menuList.removeAll()
 | 
						|
                        self.menuList.append(contentsOf: data)
 | 
						|
                        self.isActivateMenu = false
 | 
						|
                        self.selectedMenu = nil
 | 
						|
                        
 | 
						|
                        for (index, menuPreset) in self.menuList.enumerated() {
 | 
						|
                            if menuPreset.isActive {
 | 
						|
                                switch index {
 | 
						|
                                case 1:
 | 
						|
                                    self.selectMenuPreset(selectedMenuPreset: .MENU_2)
 | 
						|
                                    
 | 
						|
                                case 2:
 | 
						|
                                    self.selectMenuPreset(selectedMenuPreset: .MENU_3)
 | 
						|
                                    
 | 
						|
                                default:
 | 
						|
                                    self.selectMenuPreset(selectedMenuPreset: .MENU_1)
 | 
						|
                                }
 | 
						|
                                
 | 
						|
                                self.isActivateMenu = true
 | 
						|
                            }
 | 
						|
                            
 | 
						|
                        }
 | 
						|
                    } else {
 | 
						|
                        onFailure()
 | 
						|
                        if let message = decoded.message {
 | 
						|
                            self.errorMessage = message
 | 
						|
                        } else {
 | 
						|
                            self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
						|
                        }
 | 
						|
                        
 | 
						|
                        self.isShowPopup = true
 | 
						|
                    }
 | 
						|
                } catch {
 | 
						|
                    onFailure()
 | 
						|
                    self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
						|
                    self.isShowPopup = true
 | 
						|
                }
 | 
						|
            }
 | 
						|
            .store(in: &subscription)
 | 
						|
    }
 | 
						|
    
 | 
						|
    func shareRoom() {        
 | 
						|
        if let liveRoomInfo = self.liveRoomInfo {
 | 
						|
            let params = [
 | 
						|
                "af_dp": "voiceon://",
 | 
						|
                "deep_link_value": "live",
 | 
						|
                "deep_link_sub5": "\(AppState.shared.roomId)"
 | 
						|
            ]
 | 
						|
            
 | 
						|
            if let shareUrl = createOneLinkUrlWithURLComponents(params: params) {
 | 
						|
                if liveRoomInfo.isPrivateRoom {
 | 
						|
                    shareMessage = "\(UserDefaults.string(forKey: .nickname))님이 귀하를 보이스온 비공개라이브에 초대하였습니다.\n" +
 | 
						|
                    "※ 라이브 참여: \(shareUrl)\n" +
 | 
						|
                    "(입장 비밀번호: \(liveRoomInfo.password!))"
 | 
						|
                } else {
 | 
						|
                    shareMessage = "\(UserDefaults.string(forKey: .nickname))님이 귀하를 보이스온 공개라이브에 초대하였습니다.\n" +
 | 
						|
                    "※ 라이브 참여: \(shareUrl)"
 | 
						|
                }
 | 
						|
                
 | 
						|
                isShowShareView = true
 | 
						|
            } else {
 | 
						|
                self.errorMessage = "공유링크를 생성하지 못했습니다.\n다시 시도해 주세요."
 | 
						|
                self.isShowErrorPopup = true
 | 
						|
                return
 | 
						|
            }
 | 
						|
        } else {
 | 
						|
            self.errorMessage = "공유링크를 생성하지 못했습니다.\n다시 시도해 주세요."
 | 
						|
            self.isShowErrorPopup = true
 | 
						|
            return
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    func kickOut() {
 | 
						|
        if UserDefaults.int(forKey: .userId) == liveRoomInfo?.creatorId {
 | 
						|
            repository.kickOut(roomId: AppState.shared.roomId, userId: kickOutId)
 | 
						|
                .sink { result in
 | 
						|
                    switch result {
 | 
						|
                    case .finished:
 | 
						|
                        DEBUG_LOG("finish")
 | 
						|
                    case .failure(let error):
 | 
						|
                        ERROR_LOG(error.localizedDescription)
 | 
						|
                    }
 | 
						|
                } receiveValue: { _ in
 | 
						|
                    
 | 
						|
                }
 | 
						|
                .store(in: &subscription)
 | 
						|
            
 | 
						|
            let nickname = getUserNicknameAndProfileUrl(accountId: kickOutId).nickname
 | 
						|
            agora.sendMessageToPeer(peerId: String(kickOutId), rawMessage: LiveRoomRequestType.KICK_OUT.rawValue.data(using: .utf8)!) { [unowned self] _, error in
 | 
						|
                self.popupContent = "\(nickname)님을 내보냈습니다."
 | 
						|
                self.isShowPopup = true
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        if let index = muteSpeakers.firstIndex(of: UInt(kickOutId)) {
 | 
						|
            muteSpeakers.remove(at: index)
 | 
						|
        }
 | 
						|
        
 | 
						|
        isShowKickOutPopup = false
 | 
						|
        kickOutDesc = ""
 | 
						|
        kickOutId = 0
 | 
						|
    }
 | 
						|
    
 | 
						|
    func getDonationStatus() {
 | 
						|
        isLoading = true
 | 
						|
        repository.donationStatus(roomId: AppState.shared.roomId)
 | 
						|
            .sink { result in
 | 
						|
                switch result {
 | 
						|
                case .finished:
 | 
						|
                    DEBUG_LOG("finish")
 | 
						|
                case .failure(let error):
 | 
						|
                    ERROR_LOG(error.localizedDescription)
 | 
						|
                }
 | 
						|
            } receiveValue: { [unowned self] response in
 | 
						|
                self.isLoading = false
 | 
						|
                let responseData = response.data
 | 
						|
                
 | 
						|
                do {
 | 
						|
                    let jsonDecoder = JSONDecoder()
 | 
						|
                    let decoded = try jsonDecoder.decode(ApiResponse<GetLiveRoomDonationStatusResponse>.self, from: responseData)
 | 
						|
                    
 | 
						|
                    if let data = decoded.data, decoded.success {
 | 
						|
                        self.donationStatus = data
 | 
						|
                    } else {
 | 
						|
                        self.errorMessage = "후원현황을 가져오지 못했습니다\n다시 시도해 주세요."
 | 
						|
                        self.isShowPopup = true
 | 
						|
                    }
 | 
						|
                } catch {
 | 
						|
                    self.isLoading = false
 | 
						|
                    self.errorMessage = "후원현황을 가져오지 못했습니다\n다시 시도해 주세요."
 | 
						|
                    self.isShowPopup = true
 | 
						|
                }
 | 
						|
            }
 | 
						|
            .store(in: &subscription)
 | 
						|
    }
 | 
						|
    
 | 
						|
    func getHeartStatus() {
 | 
						|
        isLoading = true
 | 
						|
        repository.heartStatus(roomId: AppState.shared.roomId)
 | 
						|
            .sink { result in
 | 
						|
                switch result {
 | 
						|
                case .finished:
 | 
						|
                    DEBUG_LOG("finish")
 | 
						|
                case .failure(let error):
 | 
						|
                    ERROR_LOG(error.localizedDescription)
 | 
						|
                }
 | 
						|
            } receiveValue: { [unowned self] response in
 | 
						|
                self.isLoading = false
 | 
						|
                let responseData = response.data
 | 
						|
                
 | 
						|
                do {
 | 
						|
                    let jsonDecoder = JSONDecoder()
 | 
						|
                    let decoded = try jsonDecoder.decode(ApiResponse<GetLiveRoomHeartListResponse>.self, from: responseData)
 | 
						|
                    
 | 
						|
                    if let data = decoded.data, decoded.success {
 | 
						|
                        self.heartStatus = data
 | 
						|
                    } else {
 | 
						|
                        self.errorMessage = "하트 랭킹을 가져오지 못했습니다\n다시 시도해 주세요."
 | 
						|
                        self.isShowPopup = true
 | 
						|
                    }
 | 
						|
                } catch {
 | 
						|
                    self.isLoading = false
 | 
						|
                    self.errorMessage = "하트 랭킹을 가져오지 못했습니다\n다시 시도해 주세요."
 | 
						|
                    self.isShowPopup = true
 | 
						|
                }
 | 
						|
            }
 | 
						|
            .store(in: &subscription)
 | 
						|
    }
 | 
						|
    
 | 
						|
    func creatorFollow(creatorId: Int? = nil, isGetUserProfile: Bool = false) {
 | 
						|
        var userId = 0
 | 
						|
        
 | 
						|
        if let creatorId = creatorId {
 | 
						|
            userId = creatorId
 | 
						|
        } else if let liveRoomInfo = liveRoomInfo {
 | 
						|
            userId = liveRoomInfo.creatorId
 | 
						|
        }
 | 
						|
        
 | 
						|
        if userId > 0 {
 | 
						|
            isLoading = true
 | 
						|
            
 | 
						|
            userRepository.creatorFollow(creatorId: userId)
 | 
						|
                .sink { result in
 | 
						|
                    switch result {
 | 
						|
                    case .finished:
 | 
						|
                        DEBUG_LOG("finish")
 | 
						|
                    case .failure(let error):
 | 
						|
                        ERROR_LOG(error.localizedDescription)
 | 
						|
                    }
 | 
						|
                } receiveValue: { [unowned self] response in
 | 
						|
                    self.isLoading = false
 | 
						|
                    let responseData = response.data
 | 
						|
                    
 | 
						|
                    do {
 | 
						|
                        let jsonDecoder = JSONDecoder()
 | 
						|
                        let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
 | 
						|
                        
 | 
						|
                        if decoded.success {
 | 
						|
                            self.getRoomInfo()
 | 
						|
                            
 | 
						|
                            if isGetUserProfile {
 | 
						|
                                getUserProfile(userId: userId)
 | 
						|
                            }
 | 
						|
                        } else {
 | 
						|
                            if let message = decoded.message {
 | 
						|
                                self.errorMessage = message
 | 
						|
                            } else {
 | 
						|
                                self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
						|
                            }
 | 
						|
                            
 | 
						|
                            self.isShowPopup = true
 | 
						|
                        }
 | 
						|
                    } catch {
 | 
						|
                        self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
						|
                        self.isShowPopup = true
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                .store(in: &subscription)
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    func creatorUnFollow(creatorId: Int? = nil, isGetUserProfile: Bool = false) {
 | 
						|
        var userId = 0
 | 
						|
        
 | 
						|
        if let creatorId = creatorId {
 | 
						|
            userId = creatorId
 | 
						|
        } else if let liveRoomInfo = liveRoomInfo {
 | 
						|
            userId = liveRoomInfo.creatorId
 | 
						|
        }
 | 
						|
        
 | 
						|
        if userId > 0 {
 | 
						|
            isLoading = true
 | 
						|
            
 | 
						|
            userRepository.creatorUnFollow(creatorId: userId)
 | 
						|
                .sink { result in
 | 
						|
                    switch result {
 | 
						|
                    case .finished:
 | 
						|
                        DEBUG_LOG("finish")
 | 
						|
                    case .failure(let error):
 | 
						|
                        ERROR_LOG(error.localizedDescription)
 | 
						|
                    }
 | 
						|
                } receiveValue: { [unowned self] response in
 | 
						|
                    self.isLoading = false
 | 
						|
                    let responseData = response.data
 | 
						|
                    
 | 
						|
                    do {
 | 
						|
                        let jsonDecoder = JSONDecoder()
 | 
						|
                        let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
 | 
						|
                        
 | 
						|
                        if decoded.success {
 | 
						|
                            self.getRoomInfo()
 | 
						|
                            
 | 
						|
                            if isGetUserProfile {
 | 
						|
                                getUserProfile(userId: userId)
 | 
						|
                            }
 | 
						|
                        } else {
 | 
						|
                            if let message = decoded.message {
 | 
						|
                                self.errorMessage = message
 | 
						|
                            } else {
 | 
						|
                                self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
						|
                            }
 | 
						|
                            
 | 
						|
                            self.isShowPopup = true
 | 
						|
                        }
 | 
						|
                    } catch {
 | 
						|
                        self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
						|
                        self.isShowPopup = true
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                .store(in: &subscription)
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    func getUserRank(userId: Int) -> Int {
 | 
						|
        // 방장 -> -2
 | 
						|
        // 스탭 -> -3
 | 
						|
        // 나머지 -> 체크
 | 
						|
        if userId == liveRoomInfo!.creatorId {
 | 
						|
            return -2
 | 
						|
        } else if isEqualToStaffId(creatorId: userId) {
 | 
						|
            return -3
 | 
						|
        } else {
 | 
						|
            return liveRoomInfo!.donationRankingTop3UserIds.firstIndex(of: userId) ?? -1
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func getTotalHeartCount() {
 | 
						|
        repository.getTotalHeartCount(roomId: AppState.shared.roomId)
 | 
						|
            .sink { result in
 | 
						|
                switch result {
 | 
						|
                case .finished:
 | 
						|
                    DEBUG_LOG("finish")
 | 
						|
                case .failure(let error):
 | 
						|
                    ERROR_LOG(error.localizedDescription)
 | 
						|
                }
 | 
						|
            } receiveValue: { [unowned self] response in
 | 
						|
                self.isLoading = false
 | 
						|
                let responseData = response.data
 | 
						|
                
 | 
						|
                do {
 | 
						|
                    let jsonDecoder = JSONDecoder()
 | 
						|
                    let decoded = try jsonDecoder.decode(ApiResponse<GetLiveRoomHeartTotalResponse>.self, from: responseData)
 | 
						|
                    
 | 
						|
                    if let data = decoded.data, decoded.success {
 | 
						|
                        self.totalHeartCount = data.totalHeartCount
 | 
						|
                    }
 | 
						|
                } catch {
 | 
						|
                }
 | 
						|
            }
 | 
						|
            .store(in: &subscription)
 | 
						|
    }
 | 
						|
    
 | 
						|
    func getTotalDonationCan() {
 | 
						|
        repository.getTotalDoantionCan(roomId: AppState.shared.roomId)
 | 
						|
            .sink { result in
 | 
						|
                switch result {
 | 
						|
                case .finished:
 | 
						|
                    DEBUG_LOG("finish")
 | 
						|
                case .failure(let error):
 | 
						|
                    ERROR_LOG(error.localizedDescription)
 | 
						|
                }
 | 
						|
            } receiveValue: { [unowned self] response in
 | 
						|
                self.isLoading = false
 | 
						|
                let responseData = response.data
 | 
						|
                
 | 
						|
                do {
 | 
						|
                    let jsonDecoder = JSONDecoder()
 | 
						|
                    let decoded = try jsonDecoder.decode(ApiResponse<GetLiveRoomDonationTotalResponse>.self, from: responseData)
 | 
						|
                    
 | 
						|
                    if let data = decoded.data, decoded.success {
 | 
						|
                        self.totalDonationCan = data.totalDonationCan
 | 
						|
                    }
 | 
						|
                } catch {
 | 
						|
                }
 | 
						|
            }
 | 
						|
            .store(in: &subscription)
 | 
						|
    }
 | 
						|
    
 | 
						|
    func getMemberCan() {
 | 
						|
        userRepository.getMemberCan()
 | 
						|
            .sink { result in
 | 
						|
                switch result {
 | 
						|
                case .finished:
 | 
						|
                    DEBUG_LOG("finish")
 | 
						|
                case .failure(let error):
 | 
						|
                    ERROR_LOG(error.localizedDescription)
 | 
						|
                }
 | 
						|
            } receiveValue: { response in
 | 
						|
                let responseData = response.data
 | 
						|
                
 | 
						|
                do {
 | 
						|
                    let jsonDecoder = JSONDecoder()
 | 
						|
                    let decoded = try jsonDecoder.decode(ApiResponse<GetMemberCanResponse>.self, from: responseData)
 | 
						|
                    
 | 
						|
                    if let data = decoded.data, decoded.success {
 | 
						|
                        UserDefaults.set(data.can, forKey: .can)
 | 
						|
                    }
 | 
						|
                } catch {
 | 
						|
                }
 | 
						|
            }
 | 
						|
            .store(in: &subscription)
 | 
						|
    }
 | 
						|
    
 | 
						|
    func getDonationMessageList() {
 | 
						|
        isLoading = true
 | 
						|
        repository.getDonationMessageList(roomId: AppState.shared.roomId)
 | 
						|
            .sink { result in
 | 
						|
                switch result {
 | 
						|
                case .finished:
 | 
						|
                    DEBUG_LOG("finish")
 | 
						|
                case .failure(let error):
 | 
						|
                    ERROR_LOG(error.localizedDescription)
 | 
						|
                }
 | 
						|
            } receiveValue: { [unowned self] response in
 | 
						|
                self.isLoading = false
 | 
						|
                let responseData = response.data
 | 
						|
                
 | 
						|
                do {
 | 
						|
                    let jsonDecoder = JSONDecoder()
 | 
						|
                    let decoded = try jsonDecoder.decode(ApiResponse<[LiveRoomDonationMessage]>.self, from: responseData)
 | 
						|
                    
 | 
						|
                    if let data = decoded.data, decoded.success {
 | 
						|
                        self.donationMessageList.removeAll()
 | 
						|
                        self.donationMessageList.append(contentsOf: data)
 | 
						|
                        self.donationMessageCount = data.count
 | 
						|
                    } else {
 | 
						|
                        if let message = decoded.message {
 | 
						|
                            self.errorMessage = message
 | 
						|
                        } else {
 | 
						|
                            self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
						|
                        }
 | 
						|
                        
 | 
						|
                        self.isShowPopup = true
 | 
						|
                    }
 | 
						|
                } catch {
 | 
						|
                    self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
						|
                    self.isShowPopup = true
 | 
						|
                }
 | 
						|
            }
 | 
						|
            .store(in: &subscription)
 | 
						|
    }
 | 
						|
    
 | 
						|
    func deleteDonationMessage(uuid: String) {
 | 
						|
        isLoading = true
 | 
						|
        repository.deleteDonationMessage(roomId: AppState.shared.roomId, messageUUID: uuid)
 | 
						|
            .sink { result in
 | 
						|
                switch result {
 | 
						|
                case .finished:
 | 
						|
                    DEBUG_LOG("finish")
 | 
						|
                case .failure(let error):
 | 
						|
                    ERROR_LOG(error.localizedDescription)
 | 
						|
                }
 | 
						|
            } receiveValue: { [unowned self] response in
 | 
						|
                self.isLoading = false
 | 
						|
                let responseData = response.data
 | 
						|
                
 | 
						|
                do {
 | 
						|
                    let jsonDecoder = JSONDecoder()
 | 
						|
                    let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
 | 
						|
                    
 | 
						|
                    if decoded.success {
 | 
						|
                        self.donationMessageCount -= 1
 | 
						|
                        let filteredDonationMessageList = self.donationMessageList.filter { $0.uuid != uuid }
 | 
						|
                        self.donationMessageList.removeAll()
 | 
						|
                        self.donationMessageList.append(contentsOf: filteredDonationMessageList)
 | 
						|
                    } else {
 | 
						|
                        self.errorMessage = "메시지를 삭제하지 못했습니다.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
						|
                        self.isShowPopup = true
 | 
						|
                    }
 | 
						|
                } catch {
 | 
						|
                    self.errorMessage = "메시지를 삭제하지 못했습니다.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
						|
                    self.isShowPopup = true
 | 
						|
                }
 | 
						|
            }
 | 
						|
            .store(in: &subscription)
 | 
						|
    }
 | 
						|
    
 | 
						|
    func getUserProfile(userId: Int) {
 | 
						|
        isLoading = true
 | 
						|
        repository.getUserProfile(roomId: AppState.shared.roomId, userId: userId)
 | 
						|
            .sink { result in
 | 
						|
                switch result {
 | 
						|
                case .finished:
 | 
						|
                    DEBUG_LOG("finish")
 | 
						|
                case .failure(let error):
 | 
						|
                    ERROR_LOG(error.localizedDescription)
 | 
						|
                }
 | 
						|
            } receiveValue: { [unowned self] response in
 | 
						|
                self.isLoading = false
 | 
						|
                let responseData = response.data
 | 
						|
                
 | 
						|
                do {
 | 
						|
                    let jsonDecoder = JSONDecoder()
 | 
						|
                    let decoded = try jsonDecoder.decode(ApiResponse<GetLiveRoomUserProfileResponse>.self, from: responseData)
 | 
						|
                    
 | 
						|
                    if let data = decoded.data, decoded.success {
 | 
						|
                        userProfile = data
 | 
						|
                        isShowUserProfilePopup = true
 | 
						|
                    } else {
 | 
						|
                        if let message = decoded.message {
 | 
						|
                            self.errorMessage = message
 | 
						|
                        } else {
 | 
						|
                            self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
						|
                        }
 | 
						|
                        
 | 
						|
                        self.isShowPopup = true
 | 
						|
                    }
 | 
						|
                } catch {
 | 
						|
                    self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
						|
                    self.isShowPopup = true
 | 
						|
                }
 | 
						|
            }
 | 
						|
            .store(in: &subscription)
 | 
						|
    }
 | 
						|
    
 | 
						|
    func setManager(userId: Int) {
 | 
						|
        isLoading = true
 | 
						|
        repository.setManager(roomId: AppState.shared.roomId, userId: userId)
 | 
						|
            .sink { result in
 | 
						|
                switch result {
 | 
						|
                case .finished:
 | 
						|
                    DEBUG_LOG("finish")
 | 
						|
                case .failure(let error):
 | 
						|
                    ERROR_LOG(error.localizedDescription)
 | 
						|
                }
 | 
						|
            } receiveValue: { [unowned self] response in
 | 
						|
                self.isLoading = false
 | 
						|
                let responseData = response.data
 | 
						|
                
 | 
						|
                do {
 | 
						|
                    let jsonDecoder = JSONDecoder()
 | 
						|
                    let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
 | 
						|
                    
 | 
						|
                    if decoded.success {
 | 
						|
                        getRoomInfo()
 | 
						|
                        setManagerMessage()
 | 
						|
                        
 | 
						|
                        self.popupContent = "\(getUserNicknameAndProfileUrl(accountId: userId).nickname)님을 스탭으로 지정했습니다."
 | 
						|
                        self.isShowPopup = true
 | 
						|
                    } else {
 | 
						|
                        if let message = decoded.message {
 | 
						|
                            self.errorMessage = message
 | 
						|
                        } else {
 | 
						|
                            self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
						|
                        }
 | 
						|
                        
 | 
						|
                        self.isShowPopup = true
 | 
						|
                    }
 | 
						|
                } catch {
 | 
						|
                    self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
						|
                    self.isShowPopup = true
 | 
						|
                }
 | 
						|
            }
 | 
						|
            .store(in: &subscription)
 | 
						|
    }
 | 
						|
    
 | 
						|
    func releaseManagerMessageToPeer(userId: Int) {
 | 
						|
        agora.sendMessageToPeer(
 | 
						|
            peerId: String(userId),
 | 
						|
            rawMessage: LiveRoomRequestType.RELEASE_MANAGER.rawValue.data(using: .utf8)!,
 | 
						|
            completion: nil
 | 
						|
        )
 | 
						|
    }
 | 
						|
    
 | 
						|
    func setManagerMessageToPeer(userId: Int) {
 | 
						|
        agora.sendMessageToPeer(
 | 
						|
            peerId: String(userId),
 | 
						|
            rawMessage: LiveRoomRequestType.SET_MANAGER.rawValue.data(using: .utf8)!,
 | 
						|
            completion: nil
 | 
						|
        )
 | 
						|
    }
 | 
						|
    
 | 
						|
    func setNoChatting() {
 | 
						|
        agora.sendMessageToPeer(peerId: String(noChattingUserId), rawMessage: LiveRoomRequestType.NO_CHATTING.rawValue.data(using: .utf8)!) { [unowned self] _, error in
 | 
						|
            if error == nil {
 | 
						|
                self.popupContent = "\(noChattingUserNickname)님을 3분간 채팅금지를 하였습니다."
 | 
						|
                self.isShowPopup = true
 | 
						|
                
 | 
						|
                DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
 | 
						|
                    self.noChattingUserId = 0
 | 
						|
                    self.noChattingUserNickname = ""
 | 
						|
                    self.noChattingUserProfileUrl = ""
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func setManagerMessage() {
 | 
						|
        let setManagerMessage = LiveRoomChatRawMessage(
 | 
						|
            type: .SET_MANAGER,
 | 
						|
            message: "",
 | 
						|
            can: 0,
 | 
						|
            donationMessage: ""
 | 
						|
        )
 | 
						|
        
 | 
						|
        self.agora.sendRawMessageToGroup(rawMessage: setManagerMessage)
 | 
						|
    }
 | 
						|
    
 | 
						|
    func userBlock(onSuccess: @escaping (Int) -> Void) {
 | 
						|
        blockedMemberIdList.insert(reportUserId)
 | 
						|
        
 | 
						|
        isLoading = true
 | 
						|
        userRepository.memberBlock(userId: reportUserId)
 | 
						|
            .sink { result in
 | 
						|
                switch result {
 | 
						|
                case .finished:
 | 
						|
                    DEBUG_LOG("finish")
 | 
						|
                case .failure(let error):
 | 
						|
                    ERROR_LOG(error.localizedDescription)
 | 
						|
                }
 | 
						|
            } receiveValue: { [unowned self] response in
 | 
						|
                self.isLoading = false
 | 
						|
                let responseData = response.data
 | 
						|
                
 | 
						|
                do {
 | 
						|
                    let jsonDecoder = JSONDecoder()
 | 
						|
                    let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
 | 
						|
                    
 | 
						|
                    if decoded.success {
 | 
						|
                        self.reportMessage = "차단하였습니다."
 | 
						|
                        self.getUserProfile(userId: reportUserId)
 | 
						|
                        onSuccess(reportUserId)
 | 
						|
                        
 | 
						|
                        self.reportUserId = 0
 | 
						|
                        self.reportUserNickname = ""
 | 
						|
                        self.reportUserIsBlocked = false
 | 
						|
                    } else {
 | 
						|
                        if let message = decoded.message {
 | 
						|
                            self.reportMessage = message
 | 
						|
                        } else {
 | 
						|
                            self.reportMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
                    
 | 
						|
                    self.isShowReportPopup = true
 | 
						|
                } catch {
 | 
						|
                    self.reportMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
						|
                    self.isShowReportPopup = true
 | 
						|
                }
 | 
						|
            }
 | 
						|
            .store(in: &subscription)
 | 
						|
    }
 | 
						|
    
 | 
						|
    func userUnBlock() {
 | 
						|
        blockedMemberIdList.remove(reportUserId)
 | 
						|
        
 | 
						|
        isLoading = true
 | 
						|
        userRepository.memberUnBlock(userId: reportUserId)
 | 
						|
            .sink { result in
 | 
						|
                switch result {
 | 
						|
                case .finished:
 | 
						|
                    DEBUG_LOG("finish")
 | 
						|
                case .failure(let error):
 | 
						|
                    ERROR_LOG(error.localizedDescription)
 | 
						|
                }
 | 
						|
            } receiveValue: { [unowned self] response in
 | 
						|
                self.isLoading = false
 | 
						|
                let responseData = response.data
 | 
						|
                
 | 
						|
                do {
 | 
						|
                    let jsonDecoder = JSONDecoder()
 | 
						|
                    let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
 | 
						|
                    
 | 
						|
                    if decoded.success {
 | 
						|
                        self.reportMessage = "차단이 해제 되었습니다."
 | 
						|
                        self.getUserProfile(userId: reportUserId)
 | 
						|
                        self.reportUserId = 0
 | 
						|
                        self.reportUserNickname = ""
 | 
						|
                        self.reportUserIsBlocked = false
 | 
						|
                    } else {
 | 
						|
                        if let message = decoded.message {
 | 
						|
                            self.reportMessage = message
 | 
						|
                        } else {
 | 
						|
                            self.reportMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
                    
 | 
						|
                    self.isShowReportPopup = true
 | 
						|
                } catch {
 | 
						|
                    self.reportMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
						|
                    self.isShowReportPopup = true
 | 
						|
                }
 | 
						|
            }
 | 
						|
            .store(in: &subscription)
 | 
						|
    }
 | 
						|
    
 | 
						|
    func report(type: ReportType, reason: String = "프로필 신고") {
 | 
						|
        isLoading = true
 | 
						|
        
 | 
						|
        let request = ReportRequest(type: type, reason: reason, reportedMemberId: reportUserId, cheersId: nil, audioContentId: nil)
 | 
						|
        reportRepository.report(request: request)
 | 
						|
            .sink { result in
 | 
						|
                switch result {
 | 
						|
                case .finished:
 | 
						|
                    DEBUG_LOG("finish")
 | 
						|
                case .failure(let error):
 | 
						|
                    ERROR_LOG(error.localizedDescription)
 | 
						|
                }
 | 
						|
            } receiveValue: { [unowned self] response in
 | 
						|
                self.isLoading = false
 | 
						|
                let responseData = response.data
 | 
						|
                
 | 
						|
                self.reportUserId = 0
 | 
						|
                self.reportUserNickname = ""
 | 
						|
                self.reportUserIsBlocked = false
 | 
						|
                
 | 
						|
                do {
 | 
						|
                    let jsonDecoder = JSONDecoder()
 | 
						|
                    let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
 | 
						|
                    
 | 
						|
                    if let message = decoded.message {
 | 
						|
                        self.reportMessage = message
 | 
						|
                    } else {
 | 
						|
                        self.reportMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
						|
                    }
 | 
						|
                    
 | 
						|
                    self.isShowReportPopup = true
 | 
						|
                } catch {
 | 
						|
                    self.reportMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
						|
                    self.isShowReportPopup = true
 | 
						|
                }
 | 
						|
            }
 | 
						|
            .store(in: &subscription)
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func containNoChatRoom() -> Bool {
 | 
						|
        let noChatRoomList = getNoChatRoomListFromUserDefaults()
 | 
						|
        if let _ = noChatRoomList.firstIndex(of: liveRoomInfo!.roomId) {
 | 
						|
            return true
 | 
						|
        }
 | 
						|
        
 | 
						|
        return false
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func startNoChatting() {
 | 
						|
        isNoChatting = true
 | 
						|
        remainingNoChattingTime = noChattingTime
 | 
						|
        popupContent = "\(self.getUserNicknameAndProfileUrl(accountId: liveRoomInfo!.creatorId).nickname)님이 3분간 채팅을 금지하였습니다."
 | 
						|
        isShowPopup = true
 | 
						|
        
 | 
						|
        startCountDown()
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func startCountDown() {
 | 
						|
        // 1초마다 타이머를 실행
 | 
						|
        let queue = DispatchQueue.global(qos: .background)
 | 
						|
        timer = DispatchSource.makeTimerSource(queue: queue)
 | 
						|
        timer?.schedule(deadline: .now(), repeating: 1.0)
 | 
						|
        timer?.setEventHandler { [unowned self] in
 | 
						|
            DispatchQueue.main.async {
 | 
						|
                self.remainingNoChattingTime -= 1
 | 
						|
                
 | 
						|
                if self.remainingNoChattingTime <= 0 {
 | 
						|
                    self.isNoChatting = false
 | 
						|
                    self.timer?.cancel()
 | 
						|
                    self.removeNoChatRoom()
 | 
						|
                    self.popupContent = "채팅금지가 해제되었습니다."
 | 
						|
                    self.isShowPopup = true
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
        timer?.resume()
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func addNoChatRoom() {
 | 
						|
        var noChatRoomList = getNoChatRoomListFromUserDefaults()
 | 
						|
        noChatRoomList.append(liveRoomInfo!.roomId)
 | 
						|
        saveNoChatRoomListToUserDefaults(noChatRoomList: noChatRoomList)
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func removeNoChatRoom() {
 | 
						|
        var noChatRoomList = getNoChatRoomListFromUserDefaults()
 | 
						|
        guard let roomId = liveRoomInfo?.roomId else { return }
 | 
						|
        if let index = noChatRoomList.firstIndex(of: roomId) {
 | 
						|
            noChatRoomList.remove(at: index)
 | 
						|
        }
 | 
						|
        saveNoChatRoomListToUserDefaults(noChatRoomList: noChatRoomList)
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func getNoChatRoomListFromUserDefaults() -> [Int] {
 | 
						|
        if let noChatRoomListData = UserDefaults.data(forKey: .noChatRoomList) {
 | 
						|
            let jsonDecoder = JSONDecoder()
 | 
						|
            if let noChatRoomList = try? jsonDecoder.decode([Int].self, from: noChatRoomListData) {
 | 
						|
                return noChatRoomList
 | 
						|
            }
 | 
						|
        }
 | 
						|
        return []
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func saveNoChatRoomListToUserDefaults(noChatRoomList: [Int]) {
 | 
						|
        let jsonEncoder = JSONEncoder()
 | 
						|
        if let jsonData = try? jsonEncoder.encode(noChatRoomList) {
 | 
						|
            UserDefaults.set(jsonData, forKey: .noChatRoomList)
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    func setActiveRoulette(isActiveRoulette: Bool, message: String) {
 | 
						|
        self.popupContent = message
 | 
						|
        self.isShowPopup = true
 | 
						|
        self.agora.sendRawMessageToGroup(
 | 
						|
            rawMessage: LiveRoomChatRawMessage(
 | 
						|
                type: .TOGGLE_ROULETTE,
 | 
						|
                message: "",
 | 
						|
                can: 0,
 | 
						|
                donationMessage: "",
 | 
						|
                isActiveRoulette: isActiveRoulette
 | 
						|
            )
 | 
						|
        )
 | 
						|
    }
 | 
						|
    
 | 
						|
    func showRoulette() {
 | 
						|
        if let liveRoomInfo = liveRoomInfo, !isLoading {
 | 
						|
            self.roulettePreviewList.removeAll()
 | 
						|
            isLoading = true
 | 
						|
            
 | 
						|
            rouletteRepository.getRoulette(creatorId: liveRoomInfo.creatorId)
 | 
						|
                .sink { result in
 | 
						|
                    switch result {
 | 
						|
                    case .finished:
 | 
						|
                        DEBUG_LOG("finish")
 | 
						|
                    case .failure(let error):
 | 
						|
                        ERROR_LOG(error.localizedDescription)
 | 
						|
                    }
 | 
						|
                } receiveValue: { [unowned self] response in
 | 
						|
                    self.isLoading = false
 | 
						|
                    let responseData = response.data
 | 
						|
                    
 | 
						|
                    do {
 | 
						|
                        let jsonDecoder = JSONDecoder()
 | 
						|
                        let decoded = try jsonDecoder.decode(ApiResponse<[GetRouletteResponse]>.self, from: responseData)
 | 
						|
                        
 | 
						|
                        if let data = decoded.data, decoded.success, !data.isEmpty {
 | 
						|
                            let roulettePreviewList = data
 | 
						|
                                .filter { $0.isActive }
 | 
						|
                                .filter { !$0.items.isEmpty}
 | 
						|
                                .map { RoulettePreview(id: $0.id, can: $0.can, items: calculatePercentages(options: $0.items)) }
 | 
						|
                            self.roulettePreviewList.append(contentsOf: roulettePreviewList)
 | 
						|
                            self.isShowRoulettePreview = true
 | 
						|
                        } else {
 | 
						|
                            if let message = decoded.message {
 | 
						|
                                self.errorMessage = message
 | 
						|
                            } else {
 | 
						|
                                self.errorMessage = "룰렛을 사용할 수 없습니다. 다시 시도해 주세요."
 | 
						|
                            }
 | 
						|
                            self.isShowErrorPopup = true
 | 
						|
                        }
 | 
						|
                    } catch {
 | 
						|
                        self.errorMessage = "룰렛을 사용할 수 없습니다. 다시 시도해 주세요."
 | 
						|
                        self.isShowErrorPopup = true
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                .store(in: &subscription)
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    func spinRoulette(rouletteId: Int) {
 | 
						|
        if !isLoading {
 | 
						|
            isLoading = true
 | 
						|
            rouletteRepository.spinRoulette(request: SpinRouletteRequest(roomId: AppState.shared.roomId, rouletteId: rouletteId))
 | 
						|
                .sink { result in
 | 
						|
                    switch result {
 | 
						|
                    case .finished:
 | 
						|
                        DEBUG_LOG("finish")
 | 
						|
                    case .failure(let error):
 | 
						|
                        ERROR_LOG(error.localizedDescription)
 | 
						|
                    }
 | 
						|
                } receiveValue: { [unowned self] response in
 | 
						|
                    self.isLoading = false
 | 
						|
                    let responseData = response.data
 | 
						|
                    
 | 
						|
                    do {
 | 
						|
                        let jsonDecoder = JSONDecoder()
 | 
						|
                        let decoded = try jsonDecoder.decode(ApiResponse<SpinRouletteResponse>.self, from: responseData)
 | 
						|
                        
 | 
						|
                        if let data = decoded.data, decoded.success, !data.items.isEmpty {
 | 
						|
                            UserDefaults.set(UserDefaults.int(forKey: .can) - data.can, forKey: .can)
 | 
						|
                            self.rouletteItems.removeAll()
 | 
						|
                            self.rouletteItems.append(contentsOf: data.items.map { $0.title })
 | 
						|
                            self.rouletteSelectedItem = data.result
 | 
						|
                            self.rouletteCan = data.can
 | 
						|
                            self.isShowRoulette = true
 | 
						|
                        } else {
 | 
						|
                            if let message = decoded.message {
 | 
						|
                                self.errorMessage = message
 | 
						|
                            } else {
 | 
						|
                                self.errorMessage = "룰렛을 사용할 수 없습니다. 다시 시도해 주세요."
 | 
						|
                            }
 | 
						|
                            self.isShowErrorPopup = true
 | 
						|
                        }
 | 
						|
                    } catch {
 | 
						|
                        self.errorMessage = "룰렛을 사용할 수 없습니다. 다시 시도해 주세요."
 | 
						|
                        self.isShowErrorPopup = true
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                .store(in: &subscription)
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    func sendRouletteDonation() {
 | 
						|
        let rouletteRawMessage = LiveRoomChatRawMessage(
 | 
						|
            type: .ROULETTE_DONATION,
 | 
						|
            message: rouletteSelectedItem,
 | 
						|
            can: rouletteCan,
 | 
						|
            donationMessage: ""
 | 
						|
        )
 | 
						|
        
 | 
						|
        agora.sendRawMessageToGroup(rawMessage: rouletteRawMessage) { [unowned self] _, error in
 | 
						|
            if error == nil {
 | 
						|
                let (nickname, profileUrl) = self.getUserNicknameAndProfileUrl(accountId: UserDefaults.int(forKey: .userId))
 | 
						|
                self.messages.append(
 | 
						|
                    LiveRoomRouletteDonationChat(
 | 
						|
                        profileUrl: profileUrl,
 | 
						|
                        nickname: nickname,
 | 
						|
                        rouletteResult: rouletteSelectedItem
 | 
						|
                    )
 | 
						|
                )
 | 
						|
                
 | 
						|
                totalDonationCan += rouletteCan
 | 
						|
                self.rouletteItems.removeAll()
 | 
						|
                self.rouletteSelectedItem = ""
 | 
						|
                self.rouletteCan = 0
 | 
						|
                
 | 
						|
                self.invalidateChat()
 | 
						|
            } else {
 | 
						|
                self.refundRouletteDonation()
 | 
						|
            }
 | 
						|
        } fail: { [unowned self] in
 | 
						|
            self.refundRouletteDonation()
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    func stopPeriodicPlaybackValidation() {
 | 
						|
        periodicPlaybackTimer?.cancel()
 | 
						|
        periodicPlaybackTimer = nil
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func trackEventLiveContinuousListen30() {
 | 
						|
        userActionRepository.trackEvent(actionType: .LIVE_CONTINUOUS_LISTEN_30)
 | 
						|
            .sink { result in
 | 
						|
                switch result {
 | 
						|
                case .finished:
 | 
						|
                    DEBUG_LOG("finish")
 | 
						|
                case .failure(let error):
 | 
						|
                    ERROR_LOG(error.localizedDescription)
 | 
						|
                }
 | 
						|
            } receiveValue: { response in
 | 
						|
                DEBUG_LOG("트래킹 성공: \(response)")
 | 
						|
            }
 | 
						|
            .store(in: &subscription)
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func startPeriodicPlaybackValidation() {
 | 
						|
        let queue = DispatchQueue.global(qos: .background)
 | 
						|
        let period = DispatchTimeInterval.seconds(1800)
 | 
						|
        periodicPlaybackTimer = DispatchSource.makeTimerSource(queue: queue)
 | 
						|
        periodicPlaybackTimer?.schedule(deadline: .now() + period, repeating: period)
 | 
						|
        periodicPlaybackTimer?.setEventHandler { [weak self] in
 | 
						|
            self?.trackEventLiveContinuousListen30()
 | 
						|
        }
 | 
						|
        periodicPlaybackTimer?.resume()
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func calculatePercentages(options: [RouletteItem]) -> [RoulettePreviewItem] {
 | 
						|
        let updatedOptions = options.map { option in
 | 
						|
            return RoulettePreviewItem(title: option.title, percent: "\(String(format: "%.2f", option.percentage))%")
 | 
						|
        }
 | 
						|
        
 | 
						|
        return updatedOptions
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func refundRouletteDonation() {
 | 
						|
        isLoading = true
 | 
						|
        
 | 
						|
        rouletteRepository.refundRouletteDonation(roomId: AppState.shared.roomId)
 | 
						|
            .sink { result in
 | 
						|
                switch result {
 | 
						|
                case .finished:
 | 
						|
                    DEBUG_LOG("finish")
 | 
						|
                case .failure(let error):
 | 
						|
                    ERROR_LOG(error.localizedDescription)
 | 
						|
                }
 | 
						|
            } receiveValue: { [unowned self] response in
 | 
						|
                let responseData = response.data
 | 
						|
                
 | 
						|
                do {
 | 
						|
                    let jsonDecoder = JSONDecoder()
 | 
						|
                    let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
 | 
						|
                    
 | 
						|
                    self.isLoading = false
 | 
						|
                    
 | 
						|
                    if decoded.success {
 | 
						|
                        self.popupContent = "후원에 실패했습니다.\n다시 후원해주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
 | 
						|
                        self.isShowPopup = true
 | 
						|
                    } else {
 | 
						|
                        if let message = decoded.message {
 | 
						|
                            self.errorMessage = message
 | 
						|
                        } else {
 | 
						|
                            self.popupContent = "후원에 실패한 캔이 환불되지 않았습니다\n고객센터로 문의해주세요."
 | 
						|
                        }
 | 
						|
                        
 | 
						|
                        self.isShowPopup = true
 | 
						|
                    }
 | 
						|
                } catch {
 | 
						|
                    self.isLoading = false
 | 
						|
                    self.popupContent = "후원에 실패한 캔이 환불되지 않았습니다\n고객센터로 문의해주세요."
 | 
						|
                    self.isShowPopup = true
 | 
						|
                }
 | 
						|
            }
 | 
						|
            .store(in: &subscription)
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func addSignatureImage(imageUrl: String) {
 | 
						|
        if imageUrl.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 {
 | 
						|
            if !isShowSignatureImage {
 | 
						|
                isShowSignatureImage = true
 | 
						|
                signatureImageUrl = imageUrl
 | 
						|
            } else {
 | 
						|
                signatureImageUrls.append(imageUrl)
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func addSignature(signature: LiveRoomDonationResponse?) {
 | 
						|
        if let signature = signature {
 | 
						|
            if !isShowSignatureImage {
 | 
						|
                self.signature = signature
 | 
						|
                isShowSignatureImage = true
 | 
						|
            } else {
 | 
						|
                self.signatureList.append(signature)
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func showSignatureImage() {
 | 
						|
        if let signature = signature {
 | 
						|
            DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(signature.time)) {
 | 
						|
                if let nextSignature = self.signatureList.first {
 | 
						|
                    self.signature = nextSignature
 | 
						|
                    self.signatureList.removeFirst()
 | 
						|
                } else {
 | 
						|
                    self.signature = nil
 | 
						|
                    self.isShowSignatureImage = false
 | 
						|
                }
 | 
						|
            }
 | 
						|
        } else if signatureImageUrl.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 {
 | 
						|
            DispatchQueue.main.asyncAfter(deadline: .now() + 7) {
 | 
						|
                if let imageUrl = self.signatureImageUrls.first {
 | 
						|
                    self.signatureImageUrl = imageUrl
 | 
						|
                    self.signatureImageUrls.removeFirst()
 | 
						|
                } else {
 | 
						|
                    self.signatureImageUrl = ""
 | 
						|
                    self.isShowSignatureImage = false
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    func likeHeart() {
 | 
						|
        if isAvailableLikeHeart {
 | 
						|
            if !isLoadingLikeHeart {
 | 
						|
                isLoadingLikeHeart = true
 | 
						|
                
 | 
						|
                repository.likeHeart(roomId: AppState.shared.roomId)
 | 
						|
                    .sink { result in
 | 
						|
                        switch result {
 | 
						|
                        case .finished:
 | 
						|
                            DEBUG_LOG("finish")
 | 
						|
                        case .failure(let error):
 | 
						|
                            ERROR_LOG(error.localizedDescription)
 | 
						|
                        }
 | 
						|
                    } receiveValue: { [unowned self] response in
 | 
						|
                        self.isLoadingLikeHeart = false
 | 
						|
                        let responseData = response.data
 | 
						|
                        
 | 
						|
                        do {
 | 
						|
                            let jsonDecoder = JSONDecoder()
 | 
						|
                            let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData)
 | 
						|
                            
 | 
						|
                            if decoded.success {
 | 
						|
                                UserDefaults.set(UserDefaults.int(forKey: .can) - 1, forKey: .can)
 | 
						|
                                
 | 
						|
                                let donationRawMessage = LiveRoomChatRawMessage(
 | 
						|
                                    type: .HEART_DONATION,
 | 
						|
                                    message: "",
 | 
						|
                                    can: 1,
 | 
						|
                                    donationMessage: nil
 | 
						|
                                )
 | 
						|
                                
 | 
						|
                                agora.sendRawMessageToGroup(rawMessage: donationRawMessage) { [unowned self] _, error in
 | 
						|
                                    if error == nil {
 | 
						|
                                        let (nickname, _) = self.getUserNicknameAndProfileUrl(accountId: UserDefaults.int(forKey: .userId))
 | 
						|
                                        self.addHeartMessage(nickname: nickname)
 | 
						|
                                        
 | 
						|
                                        totalHeartCount += 1
 | 
						|
                                        addHeart()
 | 
						|
                                        
 | 
						|
                                        self.invalidateChat()
 | 
						|
                                    } else {
 | 
						|
                                        self.refundDonation()
 | 
						|
                                    }
 | 
						|
                                } fail: { [unowned self] in
 | 
						|
                                    self.refundDonation()
 | 
						|
                                }
 | 
						|
                            }
 | 
						|
                        } catch {
 | 
						|
                            refundDonation()
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
                    .store(in: &subscription)
 | 
						|
            }
 | 
						|
        } else {
 | 
						|
            isShowNoticeLikeHeart = true
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func addHeart() {
 | 
						|
        let heart = Heart(
 | 
						|
            id: UUID(),
 | 
						|
            offsetX: 0,
 | 
						|
            offsetY: 0,
 | 
						|
            opacity: 1,
 | 
						|
            speed: CGFloat.random(in: 1...3),
 | 
						|
            scale: 0.5,
 | 
						|
            direction: Bool.random() ? "left" : "right"
 | 
						|
        )
 | 
						|
        hearts.append(heart)
 | 
						|
        
 | 
						|
        if hearts.count == 1 {
 | 
						|
            startHeartTimer()
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func updateHearts() {
 | 
						|
        for i in (0..<hearts.count).reversed() {
 | 
						|
            hearts[i].offsetY -= hearts[i].speed * 2 // Y축으로 이동
 | 
						|
            hearts[i].opacity -= hearts[i].speed * 0.004444444444 // 투명도 감소
 | 
						|
            hearts[i].scale += 0.0067
 | 
						|
            
 | 
						|
            if hearts[i].direction == "left" {
 | 
						|
                hearts[i].offsetX -= 0.8
 | 
						|
                
 | 
						|
                if hearts[i].offsetX <= -22 {
 | 
						|
                    hearts[i].direction = "right"
 | 
						|
                }
 | 
						|
            } else {
 | 
						|
                hearts[i].offsetX += 0.8
 | 
						|
                
 | 
						|
                if hearts[i].offsetX >= 22 {
 | 
						|
                    hearts[i].direction = "left"
 | 
						|
                }
 | 
						|
            }
 | 
						|
            
 | 
						|
            // 화면을 벗어나거나 완전히 사라진 하트는 삭제
 | 
						|
            if hearts[i].scale >= 1 || hearts[i].opacity <= 0 || hearts[i].offsetY < -450 {
 | 
						|
                hearts.remove(at: i)
 | 
						|
                
 | 
						|
                if hearts.isEmpty {
 | 
						|
                    stopHeartTimer()
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        // 최대 하트 개수 제한
 | 
						|
        if hearts.count > 100 {
 | 
						|
            hearts.removeFirst()
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func addHeartMessage(nickname: String) {
 | 
						|
        if heartNickname != nil {
 | 
						|
            self.heartNicknameList.append(nickname)
 | 
						|
        } else {
 | 
						|
            self.heartNickname = nickname
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func showNextHeartMessage() {
 | 
						|
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
 | 
						|
            if let nextHeartNickname = self.heartNicknameList.first {
 | 
						|
                self.heartNickname = nextHeartNickname
 | 
						|
                self.heartNicknameList.removeFirst()
 | 
						|
            } else {
 | 
						|
                self.heartNickname = nil
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    func startHeartTimer() {
 | 
						|
        if heartTimer == nil {
 | 
						|
            let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.main)
 | 
						|
            timer.schedule(deadline: .now(), repeating: 0.033)
 | 
						|
            timer.setEventHandler { [unowned self] in
 | 
						|
                DispatchQueue.main.async {
 | 
						|
                    self.updateHearts()
 | 
						|
                }
 | 
						|
            }
 | 
						|
            timer.resume()
 | 
						|
            self.heartTimer = timer
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    func stopHeartTimer() {
 | 
						|
        heartTimer?.cancel()
 | 
						|
        heartTimer = nil
 | 
						|
    }
 | 
						|
    
 | 
						|
    private func invalidateChat() {
 | 
						|
        messageChangeFlag.toggle()
 | 
						|
        if messages.count > 100 {
 | 
						|
            messages.remove(at: 0)
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
extension LiveRoomViewModel: AgoraRtcEngineDelegate {
 | 
						|
    func rtcEngine(_ engine: AgoraRtcEngineKit, reportAudioVolumeIndicationOfSpeakers speakers: [AgoraRtcAudioVolumeInfo], totalVolume: Int) {
 | 
						|
        let activeSpeakerIds = speakers
 | 
						|
            .filter { $0.volume > 0 }
 | 
						|
            .map { $0.uid }
 | 
						|
        
 | 
						|
        activeSpeakers.removeAll()
 | 
						|
        activeSpeakers.append(contentsOf: activeSpeakerIds)
 | 
						|
    }
 | 
						|
    
 | 
						|
    func rtcEngine(_ engine: AgoraRtcEngineKit, didAudioMuted muted: Bool, byUid uid: UInt) {
 | 
						|
        if muted && !muteSpeakers.contains(uid){
 | 
						|
            muteSpeakers.append(uid)
 | 
						|
        } else {
 | 
						|
            if let index = muteSpeakers.firstIndex(of: uid) {
 | 
						|
                muteSpeakers.remove(at: index)
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) {
 | 
						|
        getRoomInfo()
 | 
						|
    }
 | 
						|
    
 | 
						|
    func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) {
 | 
						|
        DispatchQueue.main.async {[unowned self] in
 | 
						|
            if uid == UInt(self.liveRoomInfo!.creatorId) {
 | 
						|
                // 라이브 종료
 | 
						|
                self.liveRoomInfo = nil
 | 
						|
                self.errorMessage = "라이브가 종료되었습니다."
 | 
						|
                self.isShowErrorPopup = true
 | 
						|
            } else {
 | 
						|
                // get room info
 | 
						|
                self.getRoomInfo()
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
extension LiveRoomViewModel: AgoraRtmClientDelegate {
 | 
						|
    func rtmKit(_ rtmKit: AgoraRtmClientKit, didReceiveMessageEvent event: AgoraRtmMessageEvent) {
 | 
						|
        DEBUG_LOG("Message received.\n channel: \(event.channelName), publisher: \(event.publisher)")
 | 
						|
        
 | 
						|
        let rawMessage = event.message.rawData
 | 
						|
        let textMessage = event.message.stringData
 | 
						|
        let publisher = event.publisher
 | 
						|
        let (nickname, profileUrl) = getUserNicknameAndProfileUrl(accountId: Int(publisher)!)
 | 
						|
        
 | 
						|
        if let message = rawMessage {
 | 
						|
            let rawMessageString = String(data: message, encoding: .utf8)
 | 
						|
            
 | 
						|
            DispatchQueue.main.async { [unowned self] in
 | 
						|
                if rawMessageString == LiveRoomRequestType.CHANGE_LISTENER.rawValue {
 | 
						|
                    self.setListener()
 | 
						|
                    return
 | 
						|
                }
 | 
						|
                
 | 
						|
                if rawMessageString == LiveRoomRequestType.REQUEST_SPEAKER.rawValue {
 | 
						|
                    self.popupContent = "\(nickname)님이 스피커 요청을 했어요!\n스퍼커로 초대할까요?"
 | 
						|
                    self.popupCancelTitle = "건너뛰기"
 | 
						|
                    self.popupCancelAction = {
 | 
						|
                        self.isShowPopup = false
 | 
						|
                    }
 | 
						|
                    self.popupConfirmTitle = "스피커로 초대"
 | 
						|
                    self.popupConfirmAction = {
 | 
						|
                        self.isShowPopup = false
 | 
						|
                        if self.liveRoomInfo!.speakerList.count <= 5 {
 | 
						|
                            self.requestSpeakerAllow(publisher)
 | 
						|
                        } else {
 | 
						|
                            self.errorMessage = "스피커 정원이 초과되었습니다."
 | 
						|
                            self.isShowErrorPopup = true
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
                    self.isShowPopup = true
 | 
						|
                    return
 | 
						|
                }
 | 
						|
                
 | 
						|
                if rawMessageString == LiveRoomRequestType.INVITE_SPEAKER.rawValue && self.role == .LISTENER {
 | 
						|
                    self.popupContent = "스피커로 초대되었어요"
 | 
						|
                    self.popupCancelTitle = "다음에요"
 | 
						|
                    self.popupCancelAction = {
 | 
						|
                        self.isShowPopup = false
 | 
						|
                    }
 | 
						|
                    self.popupConfirmTitle = "스피커로 참여하기"
 | 
						|
                    self.popupConfirmAction = {
 | 
						|
                        self.isShowPopup = false
 | 
						|
                        self.setSpeaker()
 | 
						|
                    }
 | 
						|
                    self.isShowPopup = true
 | 
						|
                    return
 | 
						|
                }
 | 
						|
                
 | 
						|
                if rawMessageString == LiveRoomRequestType.REQUEST_SPEAKER_ALLOW.rawValue && self.role == .LISTENER {
 | 
						|
                    self.setSpeaker()
 | 
						|
                    return
 | 
						|
                }
 | 
						|
                
 | 
						|
                if rawMessageString == LiveRoomRequestType.KICK_OUT.rawValue {
 | 
						|
                    if let roomInfo = self.liveRoomInfo {
 | 
						|
                        self.popupContent = "\(self.getUserNicknameAndProfileUrl(accountId: roomInfo.creatorId).nickname)님이 라이브에서 내보냈습니다."
 | 
						|
                    } else {
 | 
						|
                        self.popupContent = "방장님이 라이브에서 내보냈습니다."
 | 
						|
                    }
 | 
						|
                    self.isShowPopup = true
 | 
						|
                    
 | 
						|
                    DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [unowned self] in
 | 
						|
                        self.quitRoom()
 | 
						|
                    }
 | 
						|
                    return
 | 
						|
                }
 | 
						|
                
 | 
						|
                if rawMessageString == LiveRoomRequestType.SET_MANAGER.rawValue {
 | 
						|
                    if self.role == .SPEAKER {
 | 
						|
                        self.role = .LISTENER
 | 
						|
                        self.isMute = false
 | 
						|
                        self.agora.mute(isMute)
 | 
						|
                        self.agora.setRole(role: .audience)
 | 
						|
                    }
 | 
						|
                    
 | 
						|
                    if let roomInfo = self.liveRoomInfo {
 | 
						|
                        self.popupContent = "\(self.getUserNicknameAndProfileUrl(accountId: roomInfo.creatorId).nickname)님이 스탭으로 지정했습니다."
 | 
						|
                    } else {
 | 
						|
                        self.popupContent = "방장님이 스탭으로 지정했습니다"
 | 
						|
                    }
 | 
						|
                    self.isShowPopup = true
 | 
						|
                }
 | 
						|
                
 | 
						|
                if rawMessageString == LiveRoomRequestType.RELEASE_MANAGER.rawValue {
 | 
						|
                    if let roomInfo = self.liveRoomInfo {
 | 
						|
                        self.popupContent = "\(self.getUserNicknameAndProfileUrl(accountId: roomInfo.creatorId).nickname)님이 스탭에서 해제했습니다."
 | 
						|
                    } else {
 | 
						|
                        self.popupContent = "방장님이 스탭에서 해제했습니다."
 | 
						|
                    }
 | 
						|
                    self.isShowPopup = true
 | 
						|
                }
 | 
						|
                
 | 
						|
                if rawMessageString == LiveRoomRequestType.NO_CHATTING.rawValue {
 | 
						|
                    DispatchQueue.main.async {
 | 
						|
                        self.addNoChatRoom()
 | 
						|
                        self.startNoChatting()
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                
 | 
						|
                do {
 | 
						|
                    let jsonDecoder = JSONDecoder()
 | 
						|
                    let decoded = try jsonDecoder.decode(LiveRoomChatRawMessage.self, from: message)
 | 
						|
                    
 | 
						|
                    if decoded.type == .SECRET_DONATION {
 | 
						|
                        self.messages.append(
 | 
						|
                            LiveRoomDonationChat(
 | 
						|
                                memberId: Int(publisher)!,
 | 
						|
                                profileUrl: profileUrl,
 | 
						|
                                nickname: nickname,
 | 
						|
                                chat: decoded.message,
 | 
						|
                                can: decoded.can,
 | 
						|
                                donationMessage: decoded.donationMessage ?? ""
 | 
						|
                            )
 | 
						|
                        )
 | 
						|
                        
 | 
						|
                        if let signature = decoded.signature {
 | 
						|
                            self.addSignature(signature: signature)
 | 
						|
                        } else if let imageUrl = decoded.signatureImageUrl {
 | 
						|
                            self.addSignatureImage(imageUrl: imageUrl)
 | 
						|
                        }
 | 
						|
                    } else if decoded.type == .DONATION {
 | 
						|
                        self.messages.append(
 | 
						|
                            LiveRoomDonationChat(
 | 
						|
                                memberId: Int(publisher)!,
 | 
						|
                                profileUrl: profileUrl,
 | 
						|
                                nickname: nickname,
 | 
						|
                                chat: decoded.message,
 | 
						|
                                can: decoded.can,
 | 
						|
                                donationMessage: decoded.donationMessage ?? ""
 | 
						|
                            )
 | 
						|
                        )
 | 
						|
                        
 | 
						|
                        self.totalDonationCan += decoded.can
 | 
						|
                        
 | 
						|
                        if let signature = decoded.signature {
 | 
						|
                            self.addSignature(signature: signature)
 | 
						|
                        } else if let imageUrl = decoded.signatureImageUrl {
 | 
						|
                            self.addSignatureImage(imageUrl: imageUrl)
 | 
						|
                        }
 | 
						|
                    } else if decoded.type == .ROULETTE_DONATION {
 | 
						|
                        self.messages.append(
 | 
						|
                            LiveRoomRouletteDonationChat(
 | 
						|
                                profileUrl: profileUrl,
 | 
						|
                                nickname: nickname,
 | 
						|
                                rouletteResult: decoded.message
 | 
						|
                            )
 | 
						|
                        )
 | 
						|
                        
 | 
						|
                        self.totalDonationCan += decoded.can
 | 
						|
                    } else if decoded.type == .TOGGLE_ROULETTE && decoded.isActiveRoulette != nil {
 | 
						|
                        self.isActiveRoulette = decoded.isActiveRoulette!
 | 
						|
                    } else if decoded.type == .EDIT_ROOM_INFO || decoded.type == .SET_MANAGER {
 | 
						|
                        self.getRoomInfo()
 | 
						|
                    } else if decoded.type == .HEART_DONATION {
 | 
						|
                        self.addHeartMessage(nickname: nickname)
 | 
						|
                        self.totalHeartCount += decoded.can
 | 
						|
                        self.addHeart()
 | 
						|
                    }
 | 
						|
                } catch {
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        if let message = textMessage {
 | 
						|
            let memberId = Int(publisher) ?? 0
 | 
						|
            let rank = getUserRank(userId: memberId)
 | 
						|
            
 | 
						|
            if !message.trimmingCharacters(in: .whitespaces).isEmpty && !blockedMemberIdList.contains(memberId) {
 | 
						|
                messages.append(LiveRoomNormalChat(userId: memberId, profileUrl: profileUrl, nickname: nickname, rank: rank, chat: message))
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        DispatchQueue.main.async { [unowned self] in
 | 
						|
            self.invalidateChat()
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    func rtmKit(_ rtmKit: AgoraRtmClientKit, didReceivePresenceEvent event: AgoraRtmPresenceEvent) {
 | 
						|
        DEBUG_LOG("didReceivePresenceEvent - \(event.type) - \(String(describing: event.publisher))")
 | 
						|
        let eventType = event.type
 | 
						|
        
 | 
						|
        if let memberId = event.publisher {
 | 
						|
            if eventType == .remoteJoinChannel {
 | 
						|
                getRoomInfo(userId: Int(memberId)!) { [unowned self] nickname in
 | 
						|
                    if !nickname.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && isEntryMessageEnabled {
 | 
						|
                        DispatchQueue.main.async { [unowned self] in
 | 
						|
                            self.messages.append(LiveRoomJoinChat(nickname: nickname))
 | 
						|
                            self.invalidateChat()
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            } else if eventType == .remoteLeaveChannel {
 | 
						|
                if let liveRoomInfo = liveRoomInfo, liveRoomInfo.creatorId != Int(memberId)! {
 | 
						|
                    getRoomInfo()
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
    
 | 
						|
    func rtmKit(_ rtmKit: AgoraRtmClientKit, didReceiveLinkStateEvent event: AgoraRtmLinkStateEvent) {
 | 
						|
        DEBUG_LOG("Signaling link state change current state is: \(event.currentState.rawValue) previous state is :\(event.previousState.rawValue)")
 | 
						|
    }
 | 
						|
}
 |