diff --git a/SodaLive/Resources/Assets.xcassets/ic_select_check.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_select_check.imageset/Contents.json new file mode 100644 index 0000000..1fbc101 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_select_check.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_select_check.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_select_check.imageset/ic_select_check.png b/SodaLive/Resources/Assets.xcassets/ic_select_check.imageset/ic_select_check.png new file mode 100644 index 0000000..b7ba15b Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_select_check.imageset/ic_select_check.png differ diff --git a/SodaLive/Sources/App/AppStep.swift b/SodaLive/Sources/App/AppStep.swift index 85bffda..0810f3c 100644 --- a/SodaLive/Sources/App/AppStep.swift +++ b/SodaLive/Sources/App/AppStep.swift @@ -71,4 +71,9 @@ enum AppStep { case userProfileFanTalkAll(userId: Int) case creatorNoticeWrite(notice: String) + + case createLive( + timeSettingMode: LiveRoomCreateViewModel.TimeSettingMode, + onSuccess: (CreateLiveRoomResponse) -> Void + ) } diff --git a/SodaLive/Sources/ContentView.swift b/SodaLive/Sources/ContentView.swift index b45ee54..5b6b544 100644 --- a/SodaLive/Sources/ContentView.swift +++ b/SodaLive/Sources/ContentView.swift @@ -104,6 +104,12 @@ struct ContentView: View { case .contentDetail(let contentId): ContentDetailView(contentId: contentId) + case .createLive(let timeSettingMode, let onSuccess): + LiveRoomCreateView( + timeSettingMode: timeSettingMode, + onSuccess: onSuccess + ) + default: EmptyView() .frame(width: 0, height: 0, alignment: .topLeading) diff --git a/SodaLive/Sources/Live/LiveApi.swift b/SodaLive/Sources/Live/LiveApi.swift index f0fc69e..9f02d71 100644 --- a/SodaLive/Sources/Live/LiveApi.swift +++ b/SodaLive/Sources/Live/LiveApi.swift @@ -17,6 +17,10 @@ enum LiveApi { case getRoomDetail(roomId: Int) case makeReservation(request: MakeLiveReservationRequest) case enterRoom(request: EnterOrQuitLiveRoomRequest) + case getTags + case getRecentRoomInfo + case createRoom(parameters: [MultipartFormData]) + case startLive(request: StartLiveRequest) } extension LiveApi: TargetType { @@ -49,18 +53,30 @@ extension LiveApi: TargetType { case .enterRoom: return "/live/room/enter" + + case .getTags: + return "/live/tag" + + case .getRecentRoomInfo: + return "/live/room/recent-room-info" + + case .createRoom: + return "/live/room" + + case .startLive: + return "/live/room/start" } } var method: Moya.Method { switch self { - case .roomList, .recentVisitRoomUsers, .getReservations, .getReservation, .getRoomDetail: + case .roomList, .recentVisitRoomUsers, .getReservations, .getReservation, .getRoomDetail, .getTags, .getRecentRoomInfo: return .get - case .makeReservation, .enterRoom: + case .makeReservation, .enterRoom, .createRoom: return .post - case .cancelReservation: + case .cancelReservation, .startLive: return .put } } @@ -83,7 +99,7 @@ extension LiveApi: TargetType { parameters: parameters, encoding: URLEncoding.queryString) - case .recentVisitRoomUsers: + case .recentVisitRoomUsers, .getTags, .getRecentRoomInfo: return .requestPlain case .getReservations(let isActive): @@ -117,6 +133,12 @@ extension LiveApi: TargetType { case .enterRoom(let request): return .requestJSONEncodable(request) + + case .createRoom(let parameters): + return .uploadMultipart(parameters) + + case .startLive(let request): + return .requestJSONEncodable(request) } } diff --git a/SodaLive/Sources/Live/LiveRepository.swift b/SodaLive/Sources/Live/LiveRepository.swift index 2f83900..c5926a3 100644 --- a/SodaLive/Sources/Live/LiveRepository.swift +++ b/SodaLive/Sources/Live/LiveRepository.swift @@ -44,4 +44,16 @@ final class LiveRepository { func enterRoom(request: EnterOrQuitLiveRoomRequest) -> AnyPublisher { return api.requestPublisher(.enterRoom(request: request)) } + + func getRecentRoomInfo() -> AnyPublisher { + return api.requestPublisher(.getRecentRoomInfo) + } + + func createRoom(parameters: [MultipartFormData]) -> AnyPublisher { + return api.requestPublisher(.createRoom(parameters: parameters)) + } + + func startLive(roomId: Int) -> AnyPublisher { + return api.requestPublisher(.startLive(request: StartLiveRequest(roomId: roomId))) + } } diff --git a/SodaLive/Sources/Live/LiveView.swift b/SodaLive/Sources/Live/LiveView.swift index e73b389..c45e753 100644 --- a/SodaLive/Sources/Live/LiveView.swift +++ b/SodaLive/Sources/Live/LiveView.swift @@ -59,7 +59,9 @@ struct LiveView: View { SectionLiveNowView( items: viewModel.liveNowItems, onClickParticipant: {_ in}, - onTapCreateLive: {} + onTapCreateLive: { + AppState.shared.setAppStep(step: .createLive(timeSettingMode: .NOW, onSuccess: onCreateSuccess)) + } ) .padding(.top, 40) } @@ -80,7 +82,9 @@ struct LiveView: View { onClickCancel: { viewModel.getSummary() }, onClickStart: {_ in}, onClickReservation: {_ in}, - onTapCreateLive: {} + onTapCreateLive: { + AppState.shared.setAppStep(step: .createLive(timeSettingMode: .RESERVATION, onSuccess: onCreateSuccess)) + } ) } } @@ -90,10 +94,14 @@ struct LiveView: View { viewModel.getSummary() } - Image("btn_make_live") - .padding(.trailing, 16) - .padding(.bottom, 16) - .onTapGesture {} + if !appState.isShowPlayer { + Image("btn_make_live") + .padding(.trailing, 16) + .padding(.bottom, 16) + .onTapGesture { + AppState.shared.setAppStep(step: .createLive(timeSettingMode: .NOW, onSuccess: onCreateSuccess)) + } + } } } @@ -123,6 +131,36 @@ struct LiveView: View { LoadingView() } } + .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { + GeometryReader { geo in + HStack { + Spacer() + Text(viewModel.errorMessage) + .padding(.vertical, 13.3) + .padding(.horizontal, 6.7) + .frame(width: geo.size.width - 66.7, alignment: .center) + .font(.custom(Font.medium.rawValue, size: 12)) + .background(Color(hex: "9970ff")) + .foregroundColor(Color.white) + .multilineTextAlignment(.leading) + .fixedSize(horizontal: false, vertical: true) + .cornerRadius(20) + .padding(.top, 66.7) + Spacer() + } + } + } + } + + private func onCreateSuccess(response: CreateLiveRoomResponse) { + viewModel.getSummary() + if let _ = response.channelName { + viewModel.enterRoom(roomId: response.id!) + } + } + + private func processStart(roomId: Int) { + viewModel.startLive(roomId: roomId) } } diff --git a/SodaLive/Sources/Live/LiveViewModel.swift b/SodaLive/Sources/Live/LiveViewModel.swift index a30777a..c2a90e8 100644 --- a/SodaLive/Sources/Live/LiveViewModel.swift +++ b/SodaLive/Sources/Live/LiveViewModel.swift @@ -176,6 +176,90 @@ final class LiveViewModel: ObservableObject { .store(in: &subscription) } + func enterRoom(roomId: Int, onSuccess: (() -> Void)? = nil, password: Int? = nil) { + isLoading = true + let request = EnterOrQuitLiveRoomRequest(roomId: roomId, password: password) + repository.enterRoom(request: request) + .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(ApiResponseWithoutData.self, from: responseData) + + if decoded.success { + AppState.shared.roomId = roomId + + if let onSuccess = onSuccess { + onSuccess() + } else { + if roomId > 0 { + AppState.shared.isShowPlayer = true + } + } + } else { + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "라이브에 입장하지 못했습니다.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self.isShowPopup = true + } + } catch { + self.errorMessage = "라이브에 입장하지 못했습니다.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + } + } + .store(in: &subscription) + } + + func startLive(roomId: Int) { + isLoading = true + repository.startLive(roomId: 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(ApiResponseWithoutData.self, from: responseData) + + if decoded.success { + getSummary() + enterRoom(roomId: roomId) + } else { + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self.isShowPopup = true + } + } catch { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + } + } + .store(in: &subscription) + } + private func getFollowedChannelList() { followedChannelItems.removeAll() isFollowedChannelLoading = true diff --git a/SodaLive/Sources/Live/Room/Create/CreateLiveRoomRequest.swift b/SodaLive/Sources/Live/Room/Create/CreateLiveRoomRequest.swift new file mode 100644 index 0000000..35b9524 --- /dev/null +++ b/SodaLive/Sources/Live/Room/Create/CreateLiveRoomRequest.swift @@ -0,0 +1,22 @@ +// +// CreateLiveRoomRequest.swift +// SodaLive +// +// Created by klaus on 2023/08/14. +// + +import Foundation + +struct CreateLiveRoomRequest: Encodable { + let title: String + let content: String + let coverImageUrl: String? + let tags: [String] + let numberOfPeople: Int + var isAdult: Bool = false + var price = 0 + var type: LiveRoomCreateViewModel.LiveRoomType = .OPEN + var password: String? = nil + let timezone: String = TimeZone.current.identifier + var beginDateTimeString: String? = nil +} diff --git a/SodaLive/Sources/Live/Room/Create/CreateLiveRoomResponse.swift b/SodaLive/Sources/Live/Room/Create/CreateLiveRoomResponse.swift new file mode 100644 index 0000000..e5e50ca --- /dev/null +++ b/SodaLive/Sources/Live/Room/Create/CreateLiveRoomResponse.swift @@ -0,0 +1,13 @@ +// +// CreateLiveRoomResponse.swift +// SodaLive +// +// Created by klaus on 2023/08/14. +// + +import Foundation + +struct CreateLiveRoomResponse: Decodable { + let id: Int? + let channelName: String? +} diff --git a/SodaLive/Sources/Live/Room/Create/GetRecentRoomInfoResponse.swift b/SodaLive/Sources/Live/Room/Create/GetRecentRoomInfoResponse.swift new file mode 100644 index 0000000..32b038b --- /dev/null +++ b/SodaLive/Sources/Live/Room/Create/GetRecentRoomInfoResponse.swift @@ -0,0 +1,16 @@ +// +// GetRecentRoomInfoResponse.swift +// SodaLive +// +// Created by klaus on 2023/08/14. +// + +import Foundation + +struct GetRecentRoomInfoResponse: Decodable { + let title: String + let notice: String + let coverImageUrl: String + let coverImagePath: String + let numberOfPeople: Int +} diff --git a/SodaLive/Sources/Live/Room/Create/LiveRoomCreateView.swift b/SodaLive/Sources/Live/Room/Create/LiveRoomCreateView.swift new file mode 100644 index 0000000..209ba37 --- /dev/null +++ b/SodaLive/Sources/Live/Room/Create/LiveRoomCreateView.swift @@ -0,0 +1,787 @@ +// +// LiveRoomCreateView.swift +// SodaLive +// +// Created by klaus on 2023/08/14. +// + +import SwiftUI +import Kingfisher + +struct LiveRoomCreateView: View { + + @StateObject var keyboardHandler = KeyboardHandler() + @StateObject var viewModel = LiveRoomCreateViewModel() + + @State private var isShowPhotoPicker = false + @State private var isShowSelectTagView = false + @State private var isShowSelectDateView = false + @State private var isShowSelectTimeView = false + + let timeSettingMode: LiveRoomCreateViewModel.TimeSettingMode + let onSuccess: (CreateLiveRoomResponse) -> Void + + init(timeSettingMode: LiveRoomCreateViewModel.TimeSettingMode, onSuccess: @escaping (CreateLiveRoomResponse) -> Void) { + UITextView.appearance().backgroundColor = .clear + UIScrollView.appearance().bounces = false + self.onSuccess = onSuccess + self.timeSettingMode = timeSettingMode + } + + var body: some View { + BaseView(isLoading: $viewModel.isLoading) { + GeometryReader { proxy in + ZStack { + VStack(spacing: 0) { + HStack(spacing: 0) { + Button { AppState.shared.back() } label: { + Image("ic_back") + .resizable() + .frame(width: 20, height: 20) + + Text("라이브 만들기") + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(Color(hex: "eeeeee")) + } + + Spacer() + + if viewModel.isShowGetRecentInfoButton { + Text("최근 데이터 가져오기") + .font(.custom(Font.medium.rawValue, size: 12)) + .foregroundColor(Color(hex: "9970ff")) + .padding(.vertical, 8) + .padding(.horizontal, 10.7) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke() + .foregroundColor(Color(hex: "9970ff")) + ) + .onTapGesture { + viewModel.getRecentInfo() + } + } + } + .padding(.horizontal, 13.3) + .frame(height: 50) + .background(Color.black) + + ScrollView(.vertical, showsIndicators: false) { + VStack(spacing: 0) { + VStack(spacing: 0) { + Text("썸네일") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + .padding(.horizontal, 13.3) + .padding(.top, 13.3) + .frame(width: screenSize().width, alignment: .leading) + + ZStack { + if let selectedImage = viewModel.coverImage { + Image(uiImage: selectedImage) + .resizable() + .scaledToFill() + .frame(width: 80, height: 116.8, alignment: .top) + .cornerRadius(10) + } else if let coverImageUrl = viewModel.coverImageUrl { + KFImage(URL(string: coverImageUrl)) + .resizable() + .scaledToFill() + .frame(width: 80, height: 116.8, alignment: .top) + .clipped() + .cornerRadius(10) + } else { + Image("ic_logo") + .resizable() + .scaledToFit() + .frame(width: 80, height: 116.8) + .background(Color(hex: "3e3358")) + .cornerRadius(10) + } + + Image("ic_camera") + .padding(10) + .background(Color(hex: "9970ff")) + .cornerRadius(30) + .offset(x: 40, y: 40) + } + .frame(alignment: .bottomTrailing) + .padding(.top, 13.3) + .onTapGesture { + isShowPhotoPicker = true + } + + TitleInputView() + .frame(width: screenSize().width - 26.7) + .padding(.top, 33.3) + + TagSelectView() + .frame(width: screenSize().width - 26.7) + .padding(.top, 33.3) + } + + VStack(spacing: 0) { + ContentInputView() + .frame(width: screenSize().width - 26.7) + .padding(.top, 33.3) + + if viewModel.roomType != .SECRET { + TimeSettingView() + .padding(.top, 33.3) + } + + NumberOfPeopleLimitView() + .frame(width: screenSize().width - 26.7) + .padding(.top, 33.3) + } + + VStack(spacing: 0) { + RoomTypeSettingView() + .frame(width: screenSize().width - 26.7) + .padding(.top, 33.3) + + if UserDefaults.bool(forKey: .auth) { + AdultSettingView() + .frame(width: screenSize().width - 26.7) + .padding(.top, 33.3) + } + + if UserDefaults.string(forKey: .role) == MemberRole.CREATOR.rawValue { + PriceSettingView() + .frame(width: screenSize().width - 26.7) + .padding(.top, 33.3) + } + } + + HStack(alignment: .top, spacing: 0) { + Button(action: { + viewModel.createRoom { response in + AppState.shared.back() + + DispatchQueue.main.async { + onSuccess(response) + } + } + }) { + Text("라이브 오픈하기") + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(Color.white) + .frame(width: screenSize().width - 26.7, height: 50) + .background(Color(hex: "9970ff")) + .cornerRadius(10) + .padding(.vertical, 13.3) + } + } + .frame(width: screenSize().width) + .background(Color(hex: "222222")) + .cornerRadius(16.7, corners: [.topLeft, .topRight]) + .padding(.top, 30) + + Rectangle() + .foregroundColor(Color(hex: "222222")) + .frame(width: screenSize().width, height: keyboardHandler.keyboardHeight) + + if proxy.safeAreaInsets.bottom > 0 { + Rectangle() + .foregroundColor(Color(hex: "222222")) + .frame(width: screenSize().width, height: 15.3) + } + } + } + } + + if isShowSelectDateView { + SelectDateView() + } + + if isShowSelectTimeView { + SelectTimeView() + } + + if isShowPhotoPicker { + ImagePicker( + isShowing: $isShowPhotoPicker, + selectedImage: $viewModel.coverImage, + sourceType: .photoLibrary + ) + } + + GeometryReader { proxy in + VStack { + Spacer() + LiveRoomCreateTagView( + isShowing: $isShowSelectTagView, + selectedTags: $viewModel.tags + ) + .frame(width: proxy.size.width, height: proxy.size.height * 0.9) + .offset(y: isShowSelectTagView ? 0 : proxy.size.height * 0.9) + .animation(.easeInOut(duration: 0.49), value: self.isShowSelectTagView) + } + } + .edgesIgnoringSafeArea(.bottom) + } + .onTapGesture { + hideKeyboard() + } + .edgesIgnoringSafeArea(.bottom) + } + } + .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { + GeometryReader { geo in + HStack { + Spacer() + Text(viewModel.errorMessage) + .padding(.vertical, 13.3) + .frame(width: geo.size.width - 66.7, alignment: .center) + .font(.custom(Font.medium.rawValue, size: 12)) + .background(Color(hex: "9970ff")) + .foregroundColor(Color.white) + .multilineTextAlignment(.center) + .cornerRadius(20) + .padding(.top, 66.7) + Spacer() + } + } + } + .onAppear { + viewModel.timeSettingMode = timeSettingMode + } + } + + @ViewBuilder + func TitleInputView() -> some View { + VStack(spacing: 0) { + Text("제목") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + .padding(.horizontal, 13.3) + .frame(width: screenSize().width, alignment: .leading) + + TextField("라이브 제목을 입력하세요", text: $viewModel.title) + .autocapitalization(.none) + .disableAutocorrection(true) + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "eeeeee")) + .accentColor(Color(hex: "9970ff")) + .keyboardType(.default) + .padding(.top, 12) + .padding(.horizontal, 6.7) + + Rectangle() + .frame(height: 1) + .foregroundColor(Color(hex: "909090").opacity(0.7)) + .padding(.top, 8.3) + } + } + + @ViewBuilder + func TagSelectView() -> some View { + VStack(alignment: .leading, spacing: 13.3) { + Text("관심사") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + + Button(action: { + hideKeyboard() + isShowSelectTagView = true + }) { + Text("관심사 선택") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "9970ff")) + .padding(.vertical, 13.7) + .frame(width: screenSize().width - 26.7) + .background(Color(hex: "9970ff").opacity(0.2)) + .cornerRadius(24.3) + .overlay( + RoundedRectangle(cornerRadius: 24.3) + .stroke() + .foregroundColor(Color(hex: "9970ff")) + ) + } + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 10) { + ForEach(viewModel.tags, id: \.self) { tag in + HStack(spacing: 6.7) { + Text(tag) + .font(.custom(Font.medium.rawValue, size: 14.7)) + .foregroundColor(.white) + + Image("ic_circle_x") + .onTapGesture { + if let index = viewModel.tags.firstIndex(of: tag) { + viewModel.tags.remove(at: index) + } + } + } + .padding(10) + .background(Color(hex: "9970ff")) + .cornerRadius(24.3) + } + } + } + .padding(.top, 13.3) + } + } + + @ViewBuilder + func ContentInputView() -> some View { + VStack(spacing: 13.3) { + HStack(spacing: 0) { + Text("공지") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + + Spacer() + + Text("\(viewModel.content.count)자") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "ff5c49")) + + Text(" / 1000자") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "777777")) + } + + TextViewWrapper( + text: $viewModel.content, + placeholder: viewModel.placeholder, + textColorHex: "eeeeee", + backgroundColorHex: "222222" + ) + .frame(width: screenSize().width - 26.7, height: 133.3) + .cornerRadius(6.7) + .padding(.top, 13.3) + } + } + + @ViewBuilder + func TimeSettingView() -> some View { + VStack(spacing: 0) { + Text("시간설정") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + .frame(width: screenSize().width - 26.7, alignment: .leading) + + HStack(spacing: 13.3) { + TimeSettingSelectButton( + title: "지금 즉시", + timeSettingMode: .NOW, + buttonWidth: (screenSize().width - 40) / 2 + ) + + TimeSettingSelectButton( + title: "예약 설정", + timeSettingMode: .RESERVATION, + buttonWidth: (screenSize().width - 40) / 2 + ) + } + .padding(.top, 13.3) + + if viewModel.timeSettingMode == .RESERVATION { + ReservationDateTimeView(buttonWidth: (screenSize().width - 40) / 2) + .padding(.top, 22.7) + } + } + } + + @ViewBuilder + func TimeSettingSelectButton( + title: String, + timeSettingMode: LiveRoomCreateViewModel.TimeSettingMode, + buttonWidth: CGFloat + ) -> some View { + HStack(spacing: 6.7) { + if viewModel.timeSettingMode == timeSettingMode { + Image("ic_select_check") + } + + Text(title) + .font(.custom(Font.bold.rawValue, size: 14.7)) + .foregroundColor( + viewModel.timeSettingMode == timeSettingMode ? + .white : + Color(hex: "9970ff") + ) + } + .frame(width: buttonWidth, height: 48.7) + .background( + viewModel.timeSettingMode == timeSettingMode ? + Color(hex: "9970ff") : + Color(hex: "1f1734") + ) + .cornerRadius(6.7) + .onTapGesture { + hideKeyboard() + if viewModel.timeSettingMode != timeSettingMode { + viewModel.timeSettingMode = timeSettingMode + } + } + } + + @ViewBuilder + func ReservationDateTimeView(buttonWidth: CGFloat) -> some View { + HStack(spacing: 13.3) { + VStack(alignment: .leading, spacing: 6.7) { + Text("예약 날짜") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "eeeeee")) + + Button(action: { + hideKeyboard() + self.isShowSelectDateView = true + }) { + Text(viewModel.reservationDateString) + .font(.custom(Font.medium.rawValue, size: 14.7)) + .foregroundColor(Color(hex: "eeeeee")) + .frame(width: buttonWidth, height: 48.7) + .overlay( + RoundedRectangle(cornerRadius: 6.7) + .stroke(Color(hex: "9970ff"), lineWidth: 1.3) + ) + } + } + + VStack(alignment: .leading, spacing: 6.7) { + Text("예약 시간") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "eeeeee")) + + Button(action: { + hideKeyboard() + self.isShowSelectTimeView = true + }) { + Text(viewModel.reservationTimeString) + .font(.custom(Font.medium.rawValue, size: 14.7)) + .foregroundColor(Color(hex: "eeeeee")) + .frame(width: buttonWidth, height: 48.7) + .overlay( + RoundedRectangle(cornerRadius: 6.7) + .stroke(Color(hex: "9970ff"), lineWidth: 1.3) + ) + } + } + } + .frame(width: screenSize().width) + .padding(.vertical, 13.3) + .background(Color(hex: "222222")) + } + + @ViewBuilder + func NumberOfPeopleLimitView() -> some View { + VStack(spacing: 13.3) { + Text("참여인원 설정") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + .frame(width: screenSize().width - 26.7, alignment: .leading) + + TextField("최대 인원 999명", text: $viewModel.numberOfPeople) + .autocapitalization(.none) + .disableAutocorrection(true) + .multilineTextAlignment(.center) + .font(.custom(Font.medium.rawValue, size: 14.7)) + .foregroundColor(Color(hex: "eeeeee")) + .accentColor(Color(hex: "9970ff")) + .keyboardType(.numberPad) + .padding(.vertical, 15.7) + .frame(width: screenSize().width - 26.7, alignment: .center) + .background(Color(hex: "222222")) + .cornerRadius(6.7) + } + } + + @ViewBuilder + func SelectDateView() -> some View { + GeometryReader { proxy in + ZStack { + Color + .black + .opacity(0.5) + .edgesIgnoringSafeArea(.all) + + VStack(spacing: 0) { + DatePicker("", selection: $viewModel.reservationDate, in: Date()..., displayedComponents: .date) + .datePickerStyle(WheelDatePickerStyle()) + .labelsHidden() + .environment(\.locale, Locale.init(identifier: "ko")) + .frame(width: proxy.size.width) + + Button(action: { self.isShowSelectDateView = false }) { + Text("확인") + .font(.system(size: 16)) + .foregroundColor(Color(hex: "eeeeee")) + .padding(.vertical, 10) + .frame(width: proxy.size.width - 53.4) + } + } + .background(Color(hex: "222222")) + .cornerRadius(6.7) + } + .frame(width: proxy.size.width) + } + } + + @ViewBuilder + func SelectTimeView() -> some View { + GeometryReader { proxy in + ZStack { + Color + .black + .opacity(0.5) + .edgesIgnoringSafeArea(.all) + + VStack(spacing: 0) { + DatePicker("", selection: $viewModel.reservationTime, displayedComponents: .hourAndMinute) + .datePickerStyle(WheelDatePickerStyle()) + .labelsHidden() + .environment(\.locale, Locale.init(identifier: "ko")) + .frame(width: proxy.size.width - 53.4) + + Button(action: { self.isShowSelectTimeView = false }) { + Text("확인") + .font(.system(size: 16)) + .foregroundColor(Color(hex: "eeeeee")) + .padding(.vertical, 10) + .frame(width: proxy.size.width) + } + } + .background(Color(hex: "222222")) + .cornerRadius(6.7) + } + .frame(width: proxy.size.width) + } + } + + @ViewBuilder + func RoomTypeSettingView() -> some View { + VStack(spacing: 0) { + Text("공개 설정") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + .frame(width: screenSize().width - 26.7, alignment: .leading) + + HStack(spacing: 13.3) { + RoomTypeSelectButton( + title: "공개", + type: .OPEN, + buttonWidth: (screenSize().width - 40) / 2 + ) + + RoomTypeSelectButton( + title: "비공개", + type: .PRIVATE, + buttonWidth: (screenSize().width - 40) / 2 + ) + } + .padding(.top, 13.3) + + if viewModel.roomType == .PRIVATE { + RoomPasswordView() + .padding(.top, 33.3) + } + } + } + + @ViewBuilder + func RoomTypeSelectButton(title: String, type: LiveRoomCreateViewModel.LiveRoomType, buttonWidth: CGFloat) -> some View { + + HStack(spacing: 6.7) { + if viewModel.roomType == type { + Image("ic_select_check") + } + + Text(title) + .font(.custom(Font.bold.rawValue, size: 14.7)) + .foregroundColor( + viewModel.roomType == type ? + .white : + Color(hex: "9970ff") + ) + } + .frame(width: buttonWidth, height: 48.7) + .background( + viewModel.roomType == type ? + Color(hex: "9970ff") : + Color(hex: "1f1734") + ) + .cornerRadius(6.7) + .onTapGesture { + hideKeyboard() + if viewModel.roomType != type { + viewModel.roomType = type + } + } + } + + @ViewBuilder + func RoomPasswordView() -> some View { + VStack(spacing: 13.3) { + Text("방 비밀번호 입력") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + .frame(width: screenSize().width - 26.7, alignment: .leading) + + TextField("방 입장 비밀번호 6자리를 입력해 주세요.", text: $viewModel.password) + .autocapitalization(.none) + .disableAutocorrection(true) + .multilineTextAlignment(.center) + .font(.custom(Font.medium.rawValue, size: 14.7)) + .foregroundColor(Color(hex: "eeeeee")) + .accentColor(Color(hex: "9970ff")) + .keyboardType(.numberPad) + .padding(.vertical, 15.7) + .frame(width: screenSize().width - 26.7, alignment: .center) + .background(Color(hex: "222222")) + .cornerRadius(6.7) + } + } + + @ViewBuilder + func AdultSettingView() -> some View { + VStack(spacing: 13.3) { + Text("연령 제한") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + .frame(width: screenSize().width - 26.7, alignment: .leading) + + HStack(spacing: 13.3) { + AdultSettingSelectButton( + title: "전체 연령", + isAdult: false, + buttonWidth: (screenSize().width - 40) / 2 + ) + + AdultSettingSelectButton( + title: "19세 이상", + isAdult: true, + buttonWidth: (screenSize().width - 40) / 2 + ) + } + } + } + + @ViewBuilder + func AdultSettingSelectButton(title: String, isAdult: Bool, buttonWidth: CGFloat) -> some View { + HStack(spacing: 6.7) { + if viewModel.isAdult == isAdult { + Image("ic_select_check") + } + + Text(title) + .font(.custom(Font.bold.rawValue, size: 14.7)) + .foregroundColor( + viewModel.isAdult == isAdult ? + .white : + Color(hex: "9970ff") + ) + } + .frame(width: buttonWidth, height: 48.7) + .background( + viewModel.isAdult == isAdult ? + Color(hex: "9970ff") : + Color(hex: "1f1734") + ) + .cornerRadius(6.7) + .onTapGesture { + hideKeyboard() + if viewModel.isAdult != isAdult { + viewModel.isAdult = isAdult + } + } + } + + @ViewBuilder + func PriceSettingView() -> some View { + VStack(spacing: 13.3) { + Text("티켓 가격") + .font(.custom(Font.bold.rawValue, size: 16.7)) + .foregroundColor(Color(hex: "eeeeee")) + .frame(width: screenSize().width - 26.7, alignment: .leading) + + HStack(spacing: 13.3) { + PriceButtonView(price: 0, buttonWidth: (screenSize().width - 53) / 3) + + PriceButtonView(price: 100, buttonWidth: (screenSize().width - 53) / 3) + + PriceButtonView(price: 300, buttonWidth: (screenSize().width - 53) / 3) + } + + HStack(spacing: 13.3) { + PriceButtonView(price: 500, buttonWidth: (screenSize().width - 53) / 3) + + PriceButtonView(price: 1000, buttonWidth: (screenSize().width - 53) / 3) + + PriceButtonView(price: 2000, buttonWidth: (screenSize().width - 53) / 3) + } + + HStack(spacing: 0) { + TextField("", text: $viewModel.priceString) + .autocapitalization(.none) + .disableAutocorrection(true) + .multilineTextAlignment(.center) + .font(.custom(Font.bold.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "9970ff")) + .accentColor(Color(hex: "9970ff")) + .keyboardType(.numberPad) + + Spacer() + + Text("코인") + .font(.custom(Font.medium.rawValue, size: 14.7)) + .foregroundColor( + Color(hex: "9970ff") + ) + } + .padding(.horizontal, 13.3) + .frame(width: screenSize().width - 26.7, height: 48.7) + .overlay( + RoundedRectangle(cornerRadius: 6.7) + .stroke( + Color(hex: !viewModel.prices.contains(viewModel.price) ? + "9970ff" : + "777777" + ), + lineWidth: 1 + ) + ) + .background( + !viewModel.prices.contains(viewModel.price) ? + Color(hex:"9970ff").opacity(0.3): + Color(hex: "232323") + ) + } + } + + @ViewBuilder + func PriceButtonView(price: Int, buttonWidth: CGFloat) -> some View { + HStack(spacing: 6.7) { + Text(price == 0 ? "무료" : "\(price) 코인") + .font(.custom( + viewModel.price == price ? + Font.bold.rawValue : + Font.medium.rawValue, + size: 14.7 + )) + .foregroundColor( + Color(hex: viewModel.price == price ? "9970ff" : "777777") + ) + } + .frame(width: buttonWidth, height: 48.7) + .overlay( + RoundedRectangle(cornerRadius: 6.7) + .stroke( + Color(hex: viewModel.price == price ? "9970ff" : "777777"), + lineWidth: 1 + ) + ) + .background( + viewModel.price == price ? + Color(hex:"9970ff").opacity(0.3): + Color(hex: "232323") + ) + .cornerRadius(6.7) + .onTapGesture { + hideKeyboard() + viewModel.priceString = "\(price)" + } + } +} diff --git a/SodaLive/Sources/Live/Room/Create/LiveRoomCreateViewModel.swift b/SodaLive/Sources/Live/Room/Create/LiveRoomCreateViewModel.swift new file mode 100644 index 0000000..705fb9e --- /dev/null +++ b/SodaLive/Sources/Live/Room/Create/LiveRoomCreateViewModel.swift @@ -0,0 +1,270 @@ +// +// LiveRoomCreateViewModel.swift +// SodaLive +// +// Created by klaus on 2023/08/14. +// + +import UIKit +import Moya +import Combine + +final class LiveRoomCreateViewModel: ObservableObject { + enum TimeSettingMode: String { + case NOW, RESERVATION + } + + enum LiveRoomType: String, Codable { + case OPEN, PRIVATE, SECRET + } + + let prices = [0, 100, 300, 500, 1000, 2000] + + @Published var isLoading = false + @Published var tags: [String] = [] + @Published var title: String = "" + @Published var content: String = "" { + didSet { + if content.count > 1000 { + content = String(content.prefix(1000)) + } + } + } + + @Published var coverImage: UIImage? = nil + @Published var coverImageUrl: String? = nil + @Published var numberOfPeople = "" + @Published var timeSettingMode: TimeSettingMode = .NOW + @Published var reservationDateString: String = Date().convertDateFormat(dateFormat: "yyyy.MM.dd") + @Published var reservationTimeString: String = Date().convertDateFormat(dateFormat: "a hh : mm") + + @Published var roomType: LiveRoomType = .OPEN { + didSet { + if roomType == .SECRET { + timeSettingMode = .NOW + } + + if roomType != .PRIVATE { + password = "" + } + } + } + @Published var password = "" { + didSet { + if password.count > 6 { + password = String(password.prefix(6)) + } + } + } + + @Published var isAdult = false + @Published var priceString = "0" { + didSet { + if priceString.count > 5 { + priceString = String(priceString.prefix(5)) + } else { + if let price = Int(priceString) { + self.price = price + } else { + self.price = 0 + } + } + } + } + @Published var price = 0 + + @Published var errorMessage = "" + @Published var isShowPopup = false + @Published var isShowGetRecentInfoButton = true + + private let repository = LiveRepository() + private var subscription = Set() + + var reservationDate = Date() { + willSet { + reservationDateString = newValue.convertDateFormat(dateFormat: "yyyy.MM.dd") + } + } + + var reservationTime = Date() { + willSet { + reservationTimeString = newValue.convertDateFormat(dateFormat: "a hh : mm") + } + } + + var coverImagePath: String? = nil + + let placeholder = "라이브 공지를 입력하세요" + + func getRecentInfo() { + isLoading = true + + repository.getRecentRoomInfo() + .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 + + DispatchQueue.global(qos: .background).async { + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponse.self, from: responseData) + + if let data = decoded.data, decoded.success { + DispatchQueue.main.async { + self.title = data.title + self.content = data.notice + self.coverImageUrl = data.coverImageUrl + self.coverImagePath = data.coverImagePath + self.numberOfPeople = String(data.numberOfPeople) + + self.errorMessage = "최근데이터를 불러왔습니다." + self.isShowPopup = true + self.isShowGetRecentInfoButton = false + } + } else { + DispatchQueue.main.async { + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "최근데이터를 불러오지 못했습니다.\n다시 시도해 주세요." + } + + self.isShowPopup = true + } + } + } catch { + print(error) + DispatchQueue.main.async { + self.errorMessage = "최근데이터를 불러오지 못했습니다.\n다시 시도해 주세요." + self.isShowPopup = true + } + } + } + } + .store(in: &subscription) + } + + func createRoom(onSuccess: @escaping (CreateLiveRoomResponse) -> Void) { + if !isLoading && validate() { + isLoading = true + + var request = CreateLiveRoomRequest( + title: title, + content: content.trimmingCharacters(in: .whitespacesAndNewlines) != placeholder ? content : "", + coverImageUrl: coverImagePath, + tags: tags, + numberOfPeople: Int(numberOfPeople)!, + isAdult: isAdult, + price: price, + type: roomType, + password: (roomType == .PRIVATE && !password.trimmingCharacters(in: .whitespaces).isEmpty) ? password : nil + ) + + if timeSettingMode == .RESERVATION && roomType != .SECRET { + request.beginDateTimeString = "\(reservationDate.convertDateFormat(dateFormat: "yyyy-MM-dd")) \(reservationTime.convertDateFormat(dateFormat: "HH:mm"))" + } + + var multipartData = [MultipartFormData]() + + let encoder = JSONEncoder() + encoder.outputFormatting = .withoutEscapingSlashes + let jsonData = try? encoder.encode(request) + + if let jsonData = jsonData { + + if let coverImage = coverImage, let imageData = coverImage.jpegData(compressionQuality: 0.8) { + multipartData.append( + MultipartFormData( + provider: .data(imageData), + name: "coverImage", + fileName: "\(UUID().uuidString)_\(Date().timeIntervalSince1970 * 1000).jpg", + mimeType: "image/*") + ) + } + + multipartData.append(MultipartFormData(provider: .data(jsonData), name: "request")) + + repository + .createRoom(parameters: multipartData) + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { 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 { + onSuccess(data) + } else { + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self.isShowPopup = true + } + } catch { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + } + } + .store(in: &subscription) + } else { + self.errorMessage = "라이브을 만들지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + self.isLoading = false + } + } + } + + private func validate() -> Bool { + if coverImage == nil && coverImagePath == nil { + self.errorMessage = "커버이미지를 선택해주세요." + self.isShowPopup = true + return false + } + + if title.trimmingCharacters(in: .whitespaces).isEmpty { + self.errorMessage = "제목을 입력해 주세요." + self.isShowPopup = true + return false + } + + let notice = content.trimmingCharacters(in: .whitespacesAndNewlines) != placeholder ? content : "" + if notice.isEmpty && notice.count < 5 { + self.errorMessage = "공지를 5자 이상 입력해주세요." + self.isShowPopup = true + return false + } + + guard let numberOfPeople = Int(numberOfPeople), (numberOfPeople >= 3 && numberOfPeople <= 999) else { + self.errorMessage = "인원을 3~999명 사이로 입력해주세요." + self.isShowPopup = true + return false + } + + if roomType == .PRIVATE && (password.trimmingCharacters(in: .whitespaces).isEmpty || password.count != 6) { + self.errorMessage = "방 입장 비밀번호 6자리를 입력해 주세요." + self.isShowPopup = true + return false + } + + return true + } +} diff --git a/SodaLive/Sources/Live/Room/Create/Tag/GetLiveRoomTagResponse.swift b/SodaLive/Sources/Live/Room/Create/Tag/GetLiveRoomTagResponse.swift new file mode 100644 index 0000000..a7607f1 --- /dev/null +++ b/SodaLive/Sources/Live/Room/Create/Tag/GetLiveRoomTagResponse.swift @@ -0,0 +1,12 @@ +// +// GetLiveRoomTagResponse.swift +// SodaLive +// +// Created by klaus on 2023/08/14. +// + +struct GetLiveRoomTagResponse: Decodable, Hashable { + let id: Int + let tag: String + let image: String +} diff --git a/SodaLive/Sources/Live/Room/Create/Tag/LiveRoomCreateTagRepository.swift b/SodaLive/Sources/Live/Room/Create/Tag/LiveRoomCreateTagRepository.swift new file mode 100644 index 0000000..85ccc18 --- /dev/null +++ b/SodaLive/Sources/Live/Room/Create/Tag/LiveRoomCreateTagRepository.swift @@ -0,0 +1,20 @@ +// +// LiveRoomCreateTagRepository.swift +// SodaLive +// +// Created by klaus on 2023/08/14. +// + +import Foundation +import CombineMoya +import Combine +import Moya + +final class LiveRoomCreateTagRepository { + private let api = MoyaProvider() + + func getTags() -> AnyPublisher { + return api.requestPublisher(.getTags) + } +} + diff --git a/SodaLive/Sources/Live/Room/Create/Tag/LiveRoomCreateTagView.swift b/SodaLive/Sources/Live/Room/Create/Tag/LiveRoomCreateTagView.swift new file mode 100644 index 0000000..6b8cddd --- /dev/null +++ b/SodaLive/Sources/Live/Room/Create/Tag/LiveRoomCreateTagView.swift @@ -0,0 +1,118 @@ +// +// LiveRoomCreateTagView.swift +// SodaLive +// +// Created by klaus on 2023/08/14. +// + +import SwiftUI +import Kingfisher + +struct LiveRoomCreateTagView: View { + + let columns = [ + GridItem(.flexible()), + GridItem(.flexible()), + GridItem(.flexible()), + GridItem(.flexible()) + ] + + @StateObject var viewModel = LiveRoomCreateTagViewModel() + + @Binding var isShowing: Bool + @Binding var selectedTags: [String] + + var body: some View { + ZStack { + Color(hex: "222222").ignoresSafeArea() + + VStack(spacing: 0) { + HStack(alignment: .top, spacing: 0) { + VStack(alignment: .leading, spacing: 6.7) { + Text("관심사 선택") + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(.white) + + Text("최대 3개까지 선택 가능합니다.") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color(hex: "777777")) + } + + Spacer() + + Image("ic_close_white") + .resizable() + .frame(width: 20, height: 20) + .onTapGesture { + isShowing = false + } + } + .padding(.horizontal, 26.7) + .padding(.top, 26.7) + + ScrollView(.vertical, showsIndicators: false) { + LazyVGrid(columns: columns, spacing: 26.7) { + ForEach(viewModel.tags, id: \.self) { tag in + VStack(spacing: 16.7) { + ZStack { + KFImage(URL(string: tag.image)) + .resizable() + .scaledToFill() + .frame(width: 60, height: 60, alignment: .top) + .clipped() + + if selectedTags.contains(tag.tag) { + Image("ic_tag_check") + .resizable() + .frame(width: 60, height: 60) + } + } + + Text(tag.tag) + .font(.custom(Font.medium.rawValue, size: 14.7)) + .foregroundColor( + selectedTags.contains(tag.tag) ? + Color(hex: "9970ff") : + Color(hex: "bbbbbb") + ) + } + .onTapGesture { + if selectedTags.contains(tag.tag) { + if let index = selectedTags.firstIndex(of: tag.tag) { + selectedTags.remove(at: index) + } + } else { + if selectedTags.count < 3 { + selectedTags.append(tag.tag) + } + } + } + } + } + } + .padding(.horizontal, 20) + .padding(.top, 26.7) + + Text("확인") + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(.white) + .padding(.vertical, 16) + .frame(width: screenSize().width - 26.7) + .background(Color(hex: "9970ff")) + .cornerRadius(10) + .padding(.bottom, 26.7) + .onTapGesture { + isShowing = false + } + } + + if viewModel.isLoading { + LoadingView() + } + } + .cornerRadius(16.7, corners: [.topLeft, .topRight]) + .onAppear { + viewModel.getTags() + } + } +} diff --git a/SodaLive/Sources/Live/Room/Create/Tag/LiveRoomCreateTagViewModel.swift b/SodaLive/Sources/Live/Room/Create/Tag/LiveRoomCreateTagViewModel.swift new file mode 100644 index 0000000..e5324d3 --- /dev/null +++ b/SodaLive/Sources/Live/Room/Create/Tag/LiveRoomCreateTagViewModel.swift @@ -0,0 +1,60 @@ +// +// LiveRoomCreateTagViewModel.swift +// SodaLive +// +// Created by klaus on 2023/08/14. +// + +import Foundation +import Combine + +final class LiveRoomCreateTagViewModel: ObservableObject { + + private let repository = LiveRoomCreateTagRepository() + private var subscription = Set() + + @Published var isLoading = false + @Published var errorMessage = "" + @Published var isShowPopup = false + @Published var tags: [GetLiveRoomTagResponse] = [] + + func getTags() { + isLoading = true + + repository.getTags() + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { response in + self.isLoading = false + let responseData = response.data + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponse<[GetLiveRoomTagResponse]>.self, from: responseData) + + if let data = decoded.data, decoded.success { + self.tags.removeAll() + self.tags.append(contentsOf: data) + } else { + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self.isShowPopup = true + } + } catch { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + } + } + .store(in: &subscription) + + } +} diff --git a/SodaLive/Sources/Live/StartLiveRequest.swift b/SodaLive/Sources/Live/StartLiveRequest.swift new file mode 100644 index 0000000..304ef9f --- /dev/null +++ b/SodaLive/Sources/Live/StartLiveRequest.swift @@ -0,0 +1,13 @@ +// +// StartLiveRequest.swift +// SodaLive +// +// Created by klaus on 2023/08/14. +// + +import Foundation + +struct StartLiveRequest: Encodable { + let roomId: Int + let timezone: String = TimeZone.current.identifier +}