parent
5682adf967
commit
0af16ac000
|
@ -8,7 +8,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum LiveRoomChatType: String {
|
enum LiveRoomChatType: String {
|
||||||
case CHAT, DONATION, JOIN
|
case CHAT, DONATION, JOIN, ROULETTE_DONATION
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol LiveRoomChat {
|
protocol LiveRoomChat {
|
||||||
|
@ -35,6 +35,14 @@ struct LiveRoomDonationChat: LiveRoomChat {
|
||||||
var type: LiveRoomChatType = .DONATION
|
var type: LiveRoomChatType = .DONATION
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct LiveRoomRouletteDonationChat: LiveRoomChat {
|
||||||
|
let profileUrl: String
|
||||||
|
let nickname: String
|
||||||
|
let rouletteResult: String
|
||||||
|
|
||||||
|
var type: LiveRoomChatType = .ROULETTE_DONATION
|
||||||
|
}
|
||||||
|
|
||||||
struct LiveRoomJoinChat: LiveRoomChat {
|
struct LiveRoomJoinChat: LiveRoomChat {
|
||||||
let nickname: String
|
let nickname: String
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import Foundation
|
||||||
|
|
||||||
struct LiveRoomChatRawMessage: Codable {
|
struct LiveRoomChatRawMessage: Codable {
|
||||||
enum LiveRoomChatRawMessageType: String, 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
|
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))
|
.background(Color(hex: "525252").opacity(0.6))
|
||||||
.cornerRadius(10)
|
.cornerRadius(10)
|
||||||
.padding(.bottom, 13.3)
|
.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 {
|
if viewModel.isLoading && viewModel.liveRoomInfo == nil {
|
||||||
LoadingView()
|
LoadingView()
|
||||||
}
|
}
|
||||||
|
@ -927,6 +943,10 @@ struct LiveRoomView: View {
|
||||||
LazyVGrid(columns: chatColumns, alignment: .leading, spacing: 20) {
|
LazyVGrid(columns: chatColumns, alignment: .leading, spacing: 20) {
|
||||||
ForEach(0..<viewModel.messages.count, id: \.self) { index in
|
ForEach(0..<viewModel.messages.count, id: \.self) { index in
|
||||||
switch (viewModel.messages[index].type) {
|
switch (viewModel.messages[index].type) {
|
||||||
|
case LiveRoomChatType.ROULETTE_DONATION:
|
||||||
|
let chatMessage = viewModel.messages[index] as! LiveRoomRouletteDonationChat
|
||||||
|
LiveRoomRouletteDonationChatItemView(chatMessage: chatMessage)
|
||||||
|
|
||||||
case LiveRoomChatType.DONATION:
|
case LiveRoomChatType.DONATION:
|
||||||
let chatMessage = viewModel.messages[index] as! LiveRoomDonationChat
|
let chatMessage = viewModel.messages[index] as! LiveRoomDonationChat
|
||||||
LiveRoomDonationChatItemView(chatMessage: chatMessage)
|
LiveRoomDonationChatItemView(chatMessage: chatMessage)
|
||||||
|
@ -955,6 +975,10 @@ struct LiveRoomView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func spinRoulette() {
|
||||||
|
viewModel.spinRoulette()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LiveRoomView_Previews: PreviewProvider {
|
struct LiveRoomView_Previews: PreviewProvider {
|
||||||
|
|
|
@ -21,6 +21,7 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
|
||||||
private let repository = LiveRepository()
|
private let repository = LiveRepository()
|
||||||
private let userRepository = UserRepository()
|
private let userRepository = UserRepository()
|
||||||
private let reportRepository = ReportRepository()
|
private let reportRepository = ReportRepository()
|
||||||
|
private let rouletteRepository = RouletteRepository()
|
||||||
private var subscription = Set<AnyCancellable>()
|
private var subscription = Set<AnyCancellable>()
|
||||||
|
|
||||||
@Published var chatMessage = ""
|
@Published var chatMessage = ""
|
||||||
|
@ -136,6 +137,14 @@ final class LiveRoomViewModel: NSObject, ObservableObject {
|
||||||
|
|
||||||
@Published var isShowRouletteSettings = false
|
@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?
|
var timer: DispatchSourceTimer?
|
||||||
|
|
||||||
func setOriginOffset(_ offset: CGFloat) {
|
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 {
|
extension LiveRoomViewModel: AgoraRtcEngineDelegate {
|
||||||
|
|
|
@ -11,6 +11,8 @@ import Moya
|
||||||
enum RouletteApi {
|
enum RouletteApi {
|
||||||
case getRoulette(creatorId: Int)
|
case getRoulette(creatorId: Int)
|
||||||
case createOrUpdateRoulette(request: CreateOrUpdateRouletteRequest)
|
case createOrUpdateRoulette(request: CreateOrUpdateRouletteRequest)
|
||||||
|
case spinRoulette(request: SpinRouletteRequest)
|
||||||
|
case refundRouletteDonation(roomId: Int)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension RouletteApi: TargetType {
|
extension RouletteApi: TargetType {
|
||||||
|
@ -22,6 +24,12 @@ extension RouletteApi: TargetType {
|
||||||
switch self {
|
switch self {
|
||||||
case .getRoulette, .createOrUpdateRoulette:
|
case .getRoulette, .createOrUpdateRoulette:
|
||||||
return "/roulette"
|
return "/roulette"
|
||||||
|
|
||||||
|
case .spinRoulette:
|
||||||
|
return "/roulette/spin"
|
||||||
|
|
||||||
|
case .refundRouletteDonation(let roomId):
|
||||||
|
return "/roulette/refund/\(roomId)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,7 +38,7 @@ extension RouletteApi: TargetType {
|
||||||
case .getRoulette:
|
case .getRoulette:
|
||||||
return .get
|
return .get
|
||||||
|
|
||||||
case .createOrUpdateRoulette:
|
case .createOrUpdateRoulette, .spinRoulette, .refundRouletteDonation:
|
||||||
return .post
|
return .post
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,6 +57,12 @@ extension RouletteApi: TargetType {
|
||||||
|
|
||||||
case .createOrUpdateRoulette(let request):
|
case .createOrUpdateRoulette(let request):
|
||||||
return .requestJSONEncodable(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> {
|
func createOrUpdateRoulette(request: CreateOrUpdateRouletteRequest) -> AnyPublisher<Response, MoyaError> {
|
||||||
return api.requestPublisher(.createOrUpdateRoulette(request: request))
|
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