diff --git a/SodaLive/Resources/Assets.xcassets/btn_add.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/btn_add.imageset/Contents.json new file mode 100644 index 0000000..eef99b1 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/btn_add.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "btn_add.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/btn_add.imageset/btn_add.png b/SodaLive/Resources/Assets.xcassets/btn_add.imageset/btn_add.png new file mode 100644 index 0000000..b36e9b5 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/btn_add.imageset/btn_add.png differ diff --git a/SodaLive/Resources/Assets.xcassets/btn_minus_round_rect.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/btn_minus_round_rect.imageset/Contents.json new file mode 100644 index 0000000..b214365 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/btn_minus_round_rect.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "btn_minus_round_rect.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/btn_minus_round_rect.imageset/btn_minus_round_rect.png b/SodaLive/Resources/Assets.xcassets/btn_minus_round_rect.imageset/btn_minus_round_rect.png new file mode 100644 index 0000000..ae6a88e Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/btn_minus_round_rect.imageset/btn_minus_round_rect.png differ diff --git a/SodaLive/Resources/Assets.xcassets/btn_plus_round_rect.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/btn_plus_round_rect.imageset/Contents.json new file mode 100644 index 0000000..6fc7e4e --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/btn_plus_round_rect.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "btn_plus_round_rect.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/btn_plus_round_rect.imageset/btn_plus_round_rect.png b/SodaLive/Resources/Assets.xcassets/btn_plus_round_rect.imageset/btn_plus_round_rect.png new file mode 100644 index 0000000..a22155e Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/btn_plus_round_rect.imageset/btn_plus_round_rect.png differ diff --git a/SodaLive/Sources/Live/Room/LiveRoomView.swift b/SodaLive/Sources/Live/Room/LiveRoomView.swift index 0961f0e..6c3a4fa 100644 --- a/SodaLive/Sources/Live/Room/LiveRoomView.swift +++ b/SodaLive/Sources/Live/Room/LiveRoomView.swift @@ -316,6 +316,9 @@ struct LiveRoomView: View { .background(Color(hex: "525252").opacity(0.6)) .cornerRadius(10) .padding(.bottom, 13.3) + .onTapGesture { + viewModel.isShowRouletteSettings = true + } } else if liveRoomInfo.creatorId != UserDefaults.int(forKey: .userId) && viewModel.isActiveRoulette { Image("ic_roulette") .resizable() @@ -682,6 +685,10 @@ struct LiveRoomView: View { } } + if viewModel.isShowRouletteSettings { + RouletteSettingsView(isShowing: $viewModel.isShowRouletteSettings) + } + if viewModel.isLoading && viewModel.liveRoomInfo == nil { LoadingView() } diff --git a/SodaLive/Sources/Live/Room/LiveRoomViewModel.swift b/SodaLive/Sources/Live/Room/LiveRoomViewModel.swift index 440078b..608de70 100644 --- a/SodaLive/Sources/Live/Room/LiveRoomViewModel.swift +++ b/SodaLive/Sources/Live/Room/LiveRoomViewModel.swift @@ -134,6 +134,8 @@ final class LiveRoomViewModel: NSObject, ObservableObject { @Published var isActiveRoulette = false + @Published var isShowRouletteSettings = false + var timer: DispatchSourceTimer? func setOriginOffset(_ offset: CGFloat) { diff --git a/SodaLive/Sources/Live/Room/Routlette/Config/RouletteOption.swift b/SodaLive/Sources/Live/Room/Routlette/Config/RouletteOption.swift new file mode 100644 index 0000000..6ddeb49 --- /dev/null +++ b/SodaLive/Sources/Live/Room/Routlette/Config/RouletteOption.swift @@ -0,0 +1,19 @@ +// +// RouletteOption.swift +// SodaLive +// +// Created by klaus on 2023/12/05. +// + +import SwiftUI + +class RouletteOption: ObservableObject { + var title: String + var weight: Int + var percentage: Int = 50 + + init(title: String, weight: Int) { + self.title = title + self.weight = weight + } +} diff --git a/SodaLive/Sources/Live/Room/Routlette/Config/RouletteSettingsOptionView.swift b/SodaLive/Sources/Live/Room/Routlette/Config/RouletteSettingsOptionView.swift new file mode 100644 index 0000000..fa42339 --- /dev/null +++ b/SodaLive/Sources/Live/Room/Routlette/Config/RouletteSettingsOptionView.swift @@ -0,0 +1,78 @@ +// +// RouletteSettingsOptionView.swift +// SodaLive +// +// Created by klaus on 2023/12/05. +// + +import SwiftUI + +struct RouletteSettingsOptionView: View { + + @ObservedObject var option: RouletteOption + + let index: Int + + let onClickPlus: () -> Void + let onClickDelete: () -> Void + let onClickSubstract: () -> Void + + var body: some View { + VStack(spacing: 6.7) { + HStack(spacing: 0) { + Text("옵션 \(index + 1)") + .font(.custom(Font.medium.rawValue, size: 14.7)) + .foregroundColor(Color(hex: "eeeeee")) + + Spacer() + + if index > 1 { + Text("삭제") + .font(.custom(Font.medium.rawValue, size: 14.7)) + .foregroundColor(Color(hex: "ff5c49")) + .onTapGesture { onClickDelete() } + } + } + + HStack(spacing: 8) { + TextField("옵션을 입력하세요", text: $option.title) + .autocapitalization(.none) + .disableAutocorrection(true) + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "eeeeee")) + .keyboardType(.default) + .padding(.horizontal, 13.3) + .padding(.vertical, 16.7) + .frame(maxWidth: .infinity) + .background(Color(hex: "222222")) + .cornerRadius(6.7) + + Text("\(option.percentage)%") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "eeeeee")) + .padding(.horizontal, 13.3) + .padding(.vertical, 16.7) + .background(Color(hex: "222222")) + .cornerRadius(6.7) + + Image("btn_minus_round_rect") + .onTapGesture { onClickSubstract() } + + Image("btn_plus_round_rect") + .onTapGesture { onClickPlus() } + } + } + } +} + +struct RouletteSettingsOptionView_Previews: PreviewProvider { + static var previews: some View { + RouletteSettingsOptionView( + option: RouletteOption(title: "옵션1", weight: 1), + index: 2, + onClickPlus: {}, + onClickDelete: {}, + onClickSubstract: {} + ) + } +} diff --git a/SodaLive/Sources/Live/Room/Routlette/Config/RouletteSettingsView.swift b/SodaLive/Sources/Live/Room/Routlette/Config/RouletteSettingsView.swift new file mode 100644 index 0000000..7074633 --- /dev/null +++ b/SodaLive/Sources/Live/Room/Routlette/Config/RouletteSettingsView.swift @@ -0,0 +1,158 @@ +// +// RouletteSettingsView.swift +// SodaLive +// +// Created by klaus on 2023/12/05. +// + +import SwiftUI + +struct RouletteSettingsView: View { + + @StateObject var keyboardHandler = KeyboardHandler() + @StateObject var viewModel = RouletteSettingsViewModel() + + @Binding var isShowing: Bool + + var body: some View { + GeometryReader { proxy in + BaseView(isLoading: $viewModel.isLoading) { + VStack(spacing: 0) { + DetailNavigationBar(title: "룰렛설정") { + isShowing = false + } + + ScrollView(.vertical, showsIndicators: false) { + VStack(spacing: 0) { + HStack(spacing: 0) { + Text("룰렛을 활성화 하시겠습니까?") + .font(.custom(Font.bold.rawValue, size: 16)) + .foregroundColor(Color(hex: "eeeeee")) + + Spacer() + + Image(viewModel.isActive ? "btn_toggle_on_big" : "btn_toggle_off_big") + .resizable() + .frame(width: 44, height: 27) + .onTapGesture { + viewModel.isActive.toggle() + } + } + + VStack(alignment: .leading, spacing: 13.3) { + Text("룰렛 금액 설정") + .font(.custom(Font.bold.rawValue, size: 16)) + .foregroundColor(Color(hex: "eeeeee")) + + HStack(spacing: 8) { + TextField("룰렛 금액을 입력해 주세요 (최소 5캔)", text: Binding( + get: { + self.viewModel.canText + }, + set: { newValue in + self.viewModel.canText = newValue.filter { "0123456789".contains($0) } + } + )) + .autocapitalization(.none) + .disableAutocorrection(true) + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "eeeeee")) + .keyboardType(.numberPad) + .padding(.horizontal, 13.3) + .padding(.vertical, 16.7) + .frame(maxWidth: .infinity) + .background(Color(hex: "222222")) + .cornerRadius(6.7) + + Spacer() + + Text("캔") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + } + } + .padding(.top, 26.7) + + VStack(alignment: .leading, spacing: 21.3) { + Text("룰렛 옵션 설정") + .font(.custom(Font.bold.rawValue, size: 16)) + .foregroundColor(Color(hex: "eeeeee")) + + HStack(spacing: 0) { + Text("※ 룰렛 옵션은 최소 2개,\n최대 6개까지 설정할 수 있습니다.") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "ff5c49")) + + Spacer() + + Image("btn_add") + .onTapGesture { viewModel.addOption() } + } + } + .padding(.top, 26.7) + + LazyVStack(spacing: 21.3) { + ForEach(viewModel.options.indices, id: \.self) { index in + RouletteSettingsOptionView( + option: viewModel.options[index], + index: index, + onClickPlus: { viewModel.plusWeight(index: index) }, + onClickDelete: { viewModel.deleteOption(index: index) }, + onClickSubstract: { viewModel.subtractWeight(index: index) } + ) + } + } + .padding(.top, 21.3) + } + .padding(.horizontal, 13.3) + } + + Spacer() + + HStack(spacing: 13.3) { + Text("미리보기") + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(Color(hex: "3bb9f1")) + .padding(.vertical, 16) + .frame(maxWidth: .infinity) + .overlay( + RoundedRectangle(cornerRadius: 10) + .strokeBorder(lineWidth: 1) + .foregroundColor(Color(hex: "3bb9f1")) + ) + .onTapGesture { + } + + Text("설정완료") + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(.white) + .padding(.vertical, 16) + .frame(maxWidth: .infinity) + .background(Color(hex: "3bb9f1")) + .cornerRadius(10) + .onTapGesture { + } + } + .padding(13.3) + .background(Color(hex: "222222")) + .cornerRadius(16.7, corners: [.topLeft, .topRight]) + + if proxy.safeAreaInsets.bottom > 0 { + Rectangle() + .foregroundColor(Color(hex: "222222")) + .frame(width: screenSize().width, height: 15.3) + } + } + } + .onAppear { + viewModel.getRoulette(creatorId: UserDefaults.int(forKey: .userId)) + } + } + } +} + +struct RouletteSettingsView_Previews: PreviewProvider { + static var previews: some View { + RouletteSettingsView(isShowing: .constant(true)) + } +} diff --git a/SodaLive/Sources/Live/Room/Routlette/Config/RouletteSettingsViewModel.swift b/SodaLive/Sources/Live/Room/Routlette/Config/RouletteSettingsViewModel.swift new file mode 100644 index 0000000..04bdef7 --- /dev/null +++ b/SodaLive/Sources/Live/Room/Routlette/Config/RouletteSettingsViewModel.swift @@ -0,0 +1,121 @@ +// +// RouletteSettingsViewModel.swift +// SodaLive +// +// Created by klaus on 2023/12/05. +// + +import Foundation +import Combine + +final class RouletteSettingsViewModel: ObservableObject { + private let repository = RouletteRepository() + private var subscription = Set() + + @Published var isLoading = false + + @Published var errorMessage = "" + @Published var isShowErrorPopup = false + + @Published var canText = ""{ + didSet { + can = Int(canText) ?? 0 + } + } + @Published var isActive = false + @Published var options = [RouletteOption]() + + var can = 5 + + func plusWeight(index: Int) { + options[index].weight += 1 + recalculatePercentages() + } + + func subtractWeight(index: Int) { + if options[index].weight > 1 { + options[index].weight -= 1 + recalculatePercentages() + } + } + + func addOption() { + if (options.count >= 6) { + return + } + options.append(RouletteOption(title: "", weight: 1)) + recalculatePercentages() + } + + func deleteOption(index: Int) { + options.remove(at: index) + recalculatePercentages() + } + + private func recalculatePercentages() { + let options = options + + var totalWeight = 0 + for option in options { + totalWeight += option.weight + } + + guard totalWeight > 0 else { return } + + for i in 0...self, from: responseData) + + if let data = decoded.data, decoded.success { + self.isActive = data.isActive + self.canText = String(data.can) + if !data.items.isEmpty { + let options = data.items.map { + RouletteOption(title: $0.title, weight: $0.weight) + } + removeAllAndAddOptions(options: options) + recalculatePercentages() + } else { + self.addOption() + self.addOption() + } + } else { + self.isActive = false + self.canText = "5" + self.addOption() + self.addOption() + } + } catch { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowErrorPopup = true + } + } + .store(in: &subscription) + } +} diff --git a/SodaLive/Sources/Live/Room/Routlette/GetRouletteResponse.swift b/SodaLive/Sources/Live/Room/Routlette/GetRouletteResponse.swift new file mode 100644 index 0000000..b159fde --- /dev/null +++ b/SodaLive/Sources/Live/Room/Routlette/GetRouletteResponse.swift @@ -0,0 +1,17 @@ +// +// GetRouletteResponse.swift +// SodaLive +// +// Created by klaus on 2023/12/06. +// + +struct GetRouletteResponse: Decodable { + let can: Int + let isActive: Bool + let items: [RouletteItem] +} + +struct RouletteItem: Decodable { + let title: String + let weight: Int +} diff --git a/SodaLive/Sources/Live/Room/Routlette/RouletteApi.swift b/SodaLive/Sources/Live/Room/Routlette/RouletteApi.swift new file mode 100644 index 0000000..14e9ec3 --- /dev/null +++ b/SodaLive/Sources/Live/Room/Routlette/RouletteApi.swift @@ -0,0 +1,51 @@ +// +// RouletteApi.swift +// SodaLive +// +// Created by klaus on 2023/12/06. +// + +import Foundation +import Moya + +enum RouletteApi { + case getRoulette(creatorId: Int) +} + +extension RouletteApi: TargetType { + var baseURL: URL { + return URL(string: BASE_URL)! + } + + var path: String { + switch self { + case .getRoulette: + return "/roulette" + } + } + + var method: Moya.Method { + switch self { + case .getRoulette: + return .get + } + } + + var task: Moya.Task { + switch self { + case .getRoulette(let creatorId): + let parameters = [ + "creatorId": creatorId + ] as [String : Any] + + return .requestParameters( + parameters: parameters, + encoding: URLEncoding.queryString + ) + } + } + + var headers: [String : String]? { + return ["Authorization": "Bearer \(UserDefaults.string(forKey: UserDefaultsKey.token))"] + } +} diff --git a/SodaLive/Sources/Live/Room/Routlette/RouletteRepository.swift b/SodaLive/Sources/Live/Room/Routlette/RouletteRepository.swift new file mode 100644 index 0000000..ea7caec --- /dev/null +++ b/SodaLive/Sources/Live/Room/Routlette/RouletteRepository.swift @@ -0,0 +1,20 @@ +// +// RouletteRepository.swift +// SodaLive +// +// Created by klaus on 2023/12/06. +// + +import Foundation +import CombineMoya +import Combine +import Moya + +final class RouletteRepository { + private let api = MoyaProvider() + + func getRoulette(creatorId: Int) -> AnyPublisher { + return api.requestPublisher(.getRoulette(creatorId: creatorId)) + } +} +