diff --git a/SodaLive/Sources/Live/Room/Chat/LiveRoomChat.swift b/SodaLive/Sources/Live/Room/Chat/LiveRoomChat.swift index c30dc33..8af0bcc 100644 --- a/SodaLive/Sources/Live/Room/Chat/LiveRoomChat.swift +++ b/SodaLive/Sources/Live/Room/Chat/LiveRoomChat.swift @@ -8,7 +8,7 @@ import Foundation enum LiveRoomChatType: String { - case CHAT, DONATION, JOIN + case CHAT, DONATION, JOIN, ROULETTE_DONATION } protocol LiveRoomChat { @@ -35,6 +35,14 @@ struct LiveRoomDonationChat: LiveRoomChat { var type: LiveRoomChatType = .DONATION } +struct LiveRoomRouletteDonationChat: LiveRoomChat { + let profileUrl: String + let nickname: String + let rouletteResult: String + + var type: LiveRoomChatType = .ROULETTE_DONATION +} + struct LiveRoomJoinChat: LiveRoomChat { let nickname: String diff --git a/SodaLive/Sources/Live/Room/Chat/LiveRoomChatRawMessage.swift b/SodaLive/Sources/Live/Room/Chat/LiveRoomChatRawMessage.swift index 42d0f42..f57b385 100644 --- a/SodaLive/Sources/Live/Room/Chat/LiveRoomChatRawMessage.swift +++ b/SodaLive/Sources/Live/Room/Chat/LiveRoomChatRawMessage.swift @@ -9,7 +9,7 @@ import Foundation struct LiveRoomChatRawMessage: Codable { enum LiveRoomChatRawMessageType: String, Codable { - case DONATION, EDIT_ROOM_INFO, SET_MANAGER, TOGGLE_ROULETTE + case DONATION, EDIT_ROOM_INFO, SET_MANAGER, TOGGLE_ROULETTE, ROULETTE_DONATION } let type: LiveRoomChatRawMessageType diff --git a/SodaLive/Sources/Live/Room/Chat/LiveRoomRouletteDonationChatItemView.swift b/SodaLive/Sources/Live/Room/Chat/LiveRoomRouletteDonationChatItemView.swift new file mode 100644 index 0000000..b8fe509 --- /dev/null +++ b/SodaLive/Sources/Live/Room/Chat/LiveRoomRouletteDonationChatItemView.swift @@ -0,0 +1,64 @@ +// +// LiveRoomRouletteDonationChatItemView.swift +// SodaLive +// +// Created by klaus on 2023/12/07. +// + +import SwiftUI +import Kingfisher + +struct LiveRoomRouletteDonationChatItemView: View { + + let chatMessage: LiveRoomRouletteDonationChat + + var body: some View { + HStack(spacing: 13.3) { + ZStack(alignment: .bottomTrailing) { + KFImage(URL(string: chatMessage.profileUrl)) + .resizable() + .scaledToFill() + .frame(width: 33.3, height: 33.3, alignment: .top) + .clipped() + .cornerRadius(23.3) + + Image("ic_roulette") + .resizable() + .frame(width: 20, height: 20) + } + + VStack(alignment: .leading, spacing: 6.7) { + HStack(spacing: 0) { + Text(chatMessage.nickname) + .font(.custom(Font.medium.rawValue, size: 12)) + .foregroundColor(.white) + + Text("님의 룰렛 결과?") + .font(.custom(Font.light.rawValue, size: 12)) + .foregroundColor(.white) + } + + HStack(spacing: 0) { + Text("[\(chatMessage.rouletteResult)]") + .font(.custom(Font.medium.rawValue, size: 13)) + .foregroundColor(Color(hex: "ffe500")) + + Text(" 당첨!") + .font(.custom(Font.medium.rawValue, size: 13)) + .foregroundColor(.white) + } + } + } + .padding(13) + .frame(width: screenSize().width - 86, alignment: .leading) + .background(Color(hex: "c25264")) + .cornerRadius(10) + .padding(.leading, 20) + } +} + +struct LiveRoomRouletteDonationChatItemView_Previews: PreviewProvider { + static var previews: some View { + LiveRoomRouletteDonationChatItemView(chatMessage: LiveRoomRouletteDonationChat(profileUrl: "", nickname: "유저일", rouletteResult: "옵션1")) + } +} diff --git a/SodaLive/Sources/Live/Room/LiveRoomView.swift b/SodaLive/Sources/Live/Room/LiveRoomView.swift index ce39819..e8cc39c 100644 --- a/SodaLive/Sources/Live/Room/LiveRoomView.swift +++ b/SodaLive/Sources/Live/Room/LiveRoomView.swift @@ -327,6 +327,9 @@ struct LiveRoomView: View { .background(Color(hex: "525252").opacity(0.6)) .cornerRadius(10) .padding(.bottom, 13.3) + .onTapGesture { + viewModel.showRoulette() + } } } @@ -691,6 +694,19 @@ struct LiveRoomView: View { } } + if let preview = viewModel.roulettePreview, viewModel.isShowRoulettePreview { + RoulettePreviewDialog( + isShowing: $viewModel.isShowRoulettePreview, + title: nil, + onClickSpin: { spinRoulette() }, + preview: preview + ) + } + + if viewModel.isShowRoulette { + + } + if viewModel.isLoading && viewModel.liveRoomInfo == nil { LoadingView() } @@ -927,6 +943,10 @@ struct LiveRoomView: View { LazyVGrid(columns: chatColumns, alignment: .leading, spacing: 20) { ForEach(0..() @Published var chatMessage = "" @@ -136,6 +137,14 @@ final class LiveRoomViewModel: NSObject, ObservableObject { @Published var isShowRouletteSettings = false + @Published var isShowRoulettePreview = false + @Published var roulettePreview: RoulettePreview? = nil + + @Published var isShowRoulette = false + @Published var rouletteItems = [RouletteItem]() + @Published var rouletteSelectedItem = "" + var rouletteCan = 0 + var timer: DispatchSourceTimer? func setOriginOffset(_ offset: CGFloat) { @@ -1342,6 +1351,196 @@ final class LiveRoomViewModel: NSObject, ObservableObject { ) ) } + + func showRoulette() { + if let liveRoomInfo = liveRoomInfo, !isLoading { + 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.self, from: responseData) + + if let data = decoded.data, decoded.success, !data.items.isEmpty { + self.roulettePreview = RoulettePreview(can: data.can, items: calculatePercentages(options: data.items)) + 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() { + if !isLoading { + isLoading = true + rouletteRepository.spinRoulette(request: SpinRouletteRequest(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.self, from: responseData) + + if let data = decoded.data, decoded.success, !data.items.isEmpty { + UserDefaults.set(UserDefaults.int(forKey: .can) - data.can, forKey: .can) + randomSelectRouletteItem(can: data.can, items: data.items) + } 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 rawMessage = rouletteSelectedItem + let rouletteRawMessage = LiveRoomChatRawMessage( + type: .ROULETTE_DONATION, + message: rawMessage, + can: rouletteCan, + donationMessage: "" + ) + + self.agora.sendRawMessageToGroup( + rawMessage: rouletteRawMessage, + completion: { [unowned self] errorCode in + if errorCode == .errorOk { + let (nickname, profileUrl) = self.getUserNicknameAndProfileUrl(accountId: UserDefaults.int(forKey: .userId)) + self.messages.append( + LiveRoomRouletteDonationChat( + profileUrl: profileUrl, + nickname: nickname, + rouletteResult: rawMessage + ) + ) + + totalDonationCan += rouletteCan + self.rouletteItems.removeAll() + self.rouletteSelectedItem = "" + self.rouletteCan = 0 + + self.messageChangeFlag.toggle() + if self.messages.count > 100 { + self.messages.remove(at: 0) + } + } else { + self.refundRouletteDonation() + } + }, + fail: { [unowned self] in + self.refundRouletteDonation() + } + ) + } + + private func calculatePercentages(options: [RouletteItem]) -> [RoulettePreviewItem] { + let totalWeight = options.reduce(0) { $0 + $1.weight } + let updatedOptions = options.map { option in + RoulettePreviewItem(title: option.title, percent: "\(Int(Float(option.weight) / Float(totalWeight) * Float(100)))%") + } + + return updatedOptions + } + + private func randomSelectRouletteItem(can: Int, items: [RouletteItem]) { + isLoading = true + + var rouletteItems = [String]() + items.forEach { + var i = 1 + while (i < $0.weight * 10) { + rouletteItems.append($0.title) + i += 1 + } + } + + isLoading = false + self.rouletteItems.removeAll() + self.rouletteItems.append(contentsOf: items) + self.rouletteSelectedItem = rouletteItems[Int(arc4random_uniform(UInt32(rouletteItems.count)))] + self.rouletteCan = can + sendRouletteDonation() + } + + 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) + } } extension LiveRoomViewModel: AgoraRtcEngineDelegate { diff --git a/SodaLive/Sources/Live/Room/Routlette/RouletteApi.swift b/SodaLive/Sources/Live/Room/Routlette/RouletteApi.swift index 5b5d13d..299f0a4 100644 --- a/SodaLive/Sources/Live/Room/Routlette/RouletteApi.swift +++ b/SodaLive/Sources/Live/Room/Routlette/RouletteApi.swift @@ -11,6 +11,8 @@ import Moya enum RouletteApi { case getRoulette(creatorId: Int) case createOrUpdateRoulette(request: CreateOrUpdateRouletteRequest) + case spinRoulette(request: SpinRouletteRequest) + case refundRouletteDonation(roomId: Int) } extension RouletteApi: TargetType { @@ -22,6 +24,12 @@ extension RouletteApi: TargetType { switch self { case .getRoulette, .createOrUpdateRoulette: return "/roulette" + + case .spinRoulette: + return "/roulette/spin" + + case .refundRouletteDonation(let roomId): + return "/roulette/refund/\(roomId)" } } @@ -30,7 +38,7 @@ extension RouletteApi: TargetType { case .getRoulette: return .get - case .createOrUpdateRoulette: + case .createOrUpdateRoulette, .spinRoulette, .refundRouletteDonation: return .post } } @@ -49,6 +57,12 @@ extension RouletteApi: TargetType { case .createOrUpdateRoulette(let request): return .requestJSONEncodable(request) + + case .spinRoulette(let request): + return .requestJSONEncodable(request) + + case .refundRouletteDonation: + return .requestPlain } } diff --git a/SodaLive/Sources/Live/Room/Routlette/RouletteRepository.swift b/SodaLive/Sources/Live/Room/Routlette/RouletteRepository.swift index 9de30e7..4ae7f61 100644 --- a/SodaLive/Sources/Live/Room/Routlette/RouletteRepository.swift +++ b/SodaLive/Sources/Live/Room/Routlette/RouletteRepository.swift @@ -20,5 +20,13 @@ final class RouletteRepository { func createOrUpdateRoulette(request: CreateOrUpdateRouletteRequest) -> AnyPublisher { return api.requestPublisher(.createOrUpdateRoulette(request: request)) } + + func spinRoulette(request: SpinRouletteRequest) -> AnyPublisher { + return api.requestPublisher(.spinRoulette(request: request)) + } + + func refundRouletteDonation(roomId: Int) -> AnyPublisher { + return api.requestPublisher(.refundRouletteDonation(roomId: roomId)) + } } diff --git a/SodaLive/Sources/Live/Room/Routlette/SpinRouletteRequest.swift b/SodaLive/Sources/Live/Room/Routlette/SpinRouletteRequest.swift new file mode 100644 index 0000000..89685c1 --- /dev/null +++ b/SodaLive/Sources/Live/Room/Routlette/SpinRouletteRequest.swift @@ -0,0 +1,13 @@ +// +// SpinRouletteRequest.swift +// SodaLive +// +// Created by klaus on 2023/12/07. +// + +import Foundation + +struct SpinRouletteRequest: Encodable { + let roomId: Int + let container: String = "ios" +}