- 룰렛 돌리기 API 연동
- 룰렛 돌린 결과 전송
This commit is contained in:
		| @@ -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 | ||||
|      | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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")) | ||||
|     } | ||||
| } | ||||
| @@ -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..<viewModel.messages.count, id: \.self) { index in | ||||
|                 switch (viewModel.messages[index].type) { | ||||
|                 case LiveRoomChatType.ROULETTE_DONATION: | ||||
|                     let chatMessage = viewModel.messages[index] as! LiveRoomRouletteDonationChat | ||||
|                     LiveRoomRouletteDonationChatItemView(chatMessage: chatMessage) | ||||
|                      | ||||
|                 case LiveRoomChatType.DONATION: | ||||
|                     let chatMessage = viewModel.messages[index] as! LiveRoomDonationChat | ||||
|                     LiveRoomDonationChatItemView(chatMessage: chatMessage) | ||||
| @@ -955,6 +975,10 @@ struct LiveRoomView: View { | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private func spinRoulette() { | ||||
|         viewModel.spinRoulette() | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct LiveRoomView_Previews: PreviewProvider { | ||||
|   | ||||
| @@ -21,6 +21,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject { | ||||
|     private let repository = LiveRepository() | ||||
|     private let userRepository = UserRepository() | ||||
|     private let reportRepository = ReportRepository() | ||||
|     private let rouletteRepository = RouletteRepository() | ||||
|     private var subscription = Set<AnyCancellable>() | ||||
|      | ||||
|     @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<GetRouletteResponse>.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<GetRouletteResponse>.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 { | ||||
|   | ||||
| @@ -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 | ||||
|         } | ||||
|     } | ||||
|      | ||||
|   | ||||
| @@ -20,5 +20,13 @@ final class RouletteRepository { | ||||
|     func createOrUpdateRoulette(request: CreateOrUpdateRouletteRequest) -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.createOrUpdateRoulette(request: request)) | ||||
|     } | ||||
|      | ||||
|     func spinRoulette(request: SpinRouletteRequest) -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.spinRoulette(request: request)) | ||||
|     } | ||||
|      | ||||
|     func refundRouletteDonation(roomId: Int) -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.refundRouletteDonation(roomId: roomId)) | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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" | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Yu Sung
					Yu Sung