parent
5682adf967
commit
0af16ac000
|
@ -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"
|
||||
}
|
Loading…
Reference in New Issue