라이브 메인 페이지
							
								
								
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/btn_item_more.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | ||||
| { | ||||
|   "images" : [ | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "1x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "2x" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "btn_item_more.png", | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "3x" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/btn_item_more.imageset/btn_item_more.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.2 KiB | 
							
								
								
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/btn_make_live.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | ||||
| { | ||||
|   "images" : [ | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "1x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "2x" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "btn_make_live.png", | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "3x" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/btn_make_live.imageset/btn_make_live.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 5.1 KiB | 
							
								
								
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/btn_toggle_off_big.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | ||||
| { | ||||
|   "images" : [ | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "1x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "2x" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "btn_toggle_off_big.png", | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "3x" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/btn_toggle_off_big.imageset/btn_toggle_off_big.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 845 B | 
							
								
								
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/btn_toggle_on_big.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | ||||
| { | ||||
|   "images" : [ | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "1x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "2x" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "btn_toggle_on_big.png", | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "3x" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/btn_toggle_on_big.imageset/btn_toggle_on_big.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 927 B | 
							
								
								
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_can.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | ||||
| { | ||||
|   "images" : [ | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "1x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "2x" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "ic_can.png", | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "3x" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_can.imageset/ic_can.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.4 KiB | 
							
								
								
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_lock.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | ||||
| { | ||||
|   "images" : [ | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "1x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "2x" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "ic_lock.png", | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "3x" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_lock.imageset/ic_lock.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 824 B | 
							
								
								
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_no_item.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | ||||
| { | ||||
|   "images" : [ | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "1x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "2x" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "ic_no_item.png", | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "3x" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_no_item.imageset/ic_no_item.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 3.8 KiB | 
							
								
								
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_plus_no_bg.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | ||||
| { | ||||
|   "images" : [ | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "1x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "2x" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "ic_plus_no_bg.png", | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "3x" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_plus_no_bg.imageset/ic_plus_no_bg.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 551 B | 
							
								
								
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/img_how_to_use.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | ||||
| { | ||||
|   "images" : [ | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "1x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "2x" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "img_how_to_use.png", | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "3x" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/img_how_to_use.imageset/img_how_to_use.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 101 KiB | 
							
								
								
									
										128
									
								
								SodaLive/Sources/Dialog/LiveRoomPasswordDialog.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,128 @@ | ||||
| // | ||||
| //  LiveRoomPasswordDialog.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/08/10. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct LiveRoomPasswordDialog: View { | ||||
|      | ||||
|     @Binding var isShowing: Bool | ||||
|      | ||||
|     let can: Int | ||||
|     let confirmAction: (Int) -> Void | ||||
|      | ||||
|     @State private var password = "" | ||||
|     @StateObject var keyboardHandler = KeyboardHandler() | ||||
|      | ||||
|     var body: some View { | ||||
|         GeometryReader { geo in | ||||
|             ZStack { | ||||
|                 Color.black | ||||
|                     .opacity(0.5) | ||||
|                     .frame(width: geo.size.width, height: geo.size.height) | ||||
|                  | ||||
|                 VStack(spacing: 0) { | ||||
|                     Text("비밀번호 입력") | ||||
|                         .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                         .foregroundColor(Color(hex: "bbbbbb")) | ||||
|                         .padding(.top, 40) | ||||
|                      | ||||
|                     Text("비공개 라이브의 입장 비밀번호를\n입력해 주세요.") | ||||
|                         .font(.custom(Font.medium.rawValue, size: 13)) | ||||
|                         .foregroundColor(Color(hex: "bbbbbb")) | ||||
|                         .multilineTextAlignment(.center) | ||||
|                         .padding(.top, 12) | ||||
|                         .padding(.horizontal, 13.3) | ||||
|                      | ||||
|                     UserTextField( | ||||
|                         title: "비밀번호", | ||||
|                         hint: "비밀번호를 입력해 주세요", | ||||
|                         isSecure: false, | ||||
|                         variable: $password, | ||||
|                         keyboardType: .numberPad | ||||
|                     ) | ||||
|                     .padding(.horizontal, 13.3) | ||||
|                     .padding(.top, 13.3) | ||||
|                      | ||||
|                     HStack(spacing: 13.3) { | ||||
|                         Text("취소") | ||||
|                             .font(.custom(Font.bold.rawValue, size: 15.3)) | ||||
|                             .foregroundColor(Color(hex: "9970ff")) | ||||
|                             .padding(.vertical, 16) | ||||
|                             .frame(width: (geo.size.width - 66.7) / 3) | ||||
|                             .background(Color(hex: "9970ff").opacity(0.13)) | ||||
|                             .cornerRadius(8) | ||||
|                             .overlay( | ||||
|                                 RoundedRectangle(cornerRadius: 8) | ||||
|                                     .stroke(Color(hex: "9970ff"), lineWidth: 1) | ||||
|                             ) | ||||
|                             .onTapGesture { | ||||
|                                 isShowing = false | ||||
|                             } | ||||
|                          | ||||
|                         if can > 0 { | ||||
|                             HStack(spacing: 0) { | ||||
|                                 Text("\(can)") | ||||
|                                     .font(.custom(Font.bold.rawValue, size: 15.3)) | ||||
|                                     .foregroundColor(Color(hex: "ffffff")) | ||||
|                                  | ||||
|                                 Image("ic_can") | ||||
|                                      | ||||
|                                 Text("으로 입장") | ||||
|                                     .font(.custom(Font.bold.rawValue, size: 15.3)) | ||||
|                                     .foregroundColor(Color(hex: "ffffff")) | ||||
|                             } | ||||
|                             .padding(.vertical, 16) | ||||
|                             .frame(width: (geo.size.width - 66.7) * 2 / 3) | ||||
|                             .background(Color(hex: "9970ff")) | ||||
|                             .cornerRadius(8) | ||||
|                             .onTapGesture { | ||||
|                                 if password.trimmingCharacters(in: .whitespaces).isEmpty { | ||||
|                                     confirmAction(0) | ||||
|                                 } else { | ||||
|                                     confirmAction(Int(password)!) | ||||
|                                 } | ||||
|                                 isShowing = false | ||||
|                             } | ||||
|                         } else { | ||||
|                             Text("입장하기") | ||||
|                                 .font(.custom(Font.bold.rawValue, size: 15.3)) | ||||
|                                 .foregroundColor(Color(hex: "ffffff")) | ||||
|                                 .padding(.vertical, 16) | ||||
|                                 .frame(width: (geo.size.width - 66.7) * 2 / 3) | ||||
|                                 .background(Color(hex: "9970ff")) | ||||
|                                 .cornerRadius(8) | ||||
|                                 .onTapGesture { | ||||
|                                     if password.trimmingCharacters(in: .whitespaces).isEmpty { | ||||
|                                         confirmAction(0) | ||||
|                                     } else { | ||||
|                                         confirmAction(Int(password)!) | ||||
|                                     } | ||||
|                                     isShowing = false | ||||
|                                 } | ||||
|                         } | ||||
|                     } | ||||
|                     .padding(.top, 45) | ||||
|                     .padding(.bottom, 16.7) | ||||
|                 } | ||||
|                 .frame(width: geo.size.width - 26.7, alignment: .center) | ||||
|                 .background(Color(hex: "222222")) | ||||
|                 .cornerRadius(10) | ||||
|                 .offset(y: 0 - (keyboardHandler.keyboardHeight / 3)) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct LiveRoomPasswordDialog_Previews: PreviewProvider { | ||||
|     static var previews: some View { | ||||
|         LiveRoomPasswordDialog( | ||||
|             isShowing: .constant(true), | ||||
|             can: 10, | ||||
|             confirmAction: { _ in } | ||||
|         ) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										107
									
								
								SodaLive/Sources/Dialog/SodaDialog.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,107 @@ | ||||
| // | ||||
| //  SodaDialog.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/08/10. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct SodaDialog: View { | ||||
|      | ||||
|     let title: String | ||||
|     let desc: String | ||||
|     let confirmButtonTitle: String | ||||
|     let confirmButtonAction: () -> Void | ||||
|     let cancelButtonTitle: String | ||||
|     let cancelButtonAction: () -> Void | ||||
|      | ||||
|     init( | ||||
|         title: String, | ||||
|         desc: String, | ||||
|         confirmButtonTitle: String, | ||||
|         confirmButtonAction: @escaping () -> Void, | ||||
|         cancelButtonTitle: String = "", | ||||
|         cancelButtonAction: @escaping () -> Void = {} | ||||
|     ) { | ||||
|         self.title = title | ||||
|         self.desc = desc | ||||
|         self.confirmButtonTitle = confirmButtonTitle | ||||
|         self.confirmButtonAction = confirmButtonAction | ||||
|         self.cancelButtonTitle = cancelButtonTitle | ||||
|         self.cancelButtonAction = cancelButtonAction | ||||
|     } | ||||
|      | ||||
|     var body: some View { | ||||
|         GeometryReader { geo in | ||||
|             ZStack { | ||||
|                 Color.black | ||||
|                     .opacity(0.5) | ||||
|                     .frame(width: geo.size.width, height: geo.size.height) | ||||
|                  | ||||
|                 VStack(spacing: 0) { | ||||
|                     Text(title) | ||||
|                         .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                         .foregroundColor(Color(hex: "bbbbbb")) | ||||
|                         .padding(.top, 40) | ||||
|                      | ||||
|                     Text(desc) | ||||
|                         .font(.custom(Font.medium.rawValue, size: 15)) | ||||
|                         .foregroundColor(Color(hex: "bbbbbb")) | ||||
|                         .multilineTextAlignment(.center) | ||||
|                         .padding(.top, 12) | ||||
|                         .padding(.horizontal, 13.3) | ||||
|                         .fixedSize(horizontal: false, vertical: true) | ||||
|                      | ||||
|                     HStack(spacing: 13.3) { | ||||
|                         if cancelButtonTitle.count > 0 { | ||||
|                             Text(cancelButtonTitle) | ||||
|                                 .font(.custom(Font.bold.rawValue, size: 15.3)) | ||||
|                                 .foregroundColor(Color(hex: "9970ff")) | ||||
|                                 .padding(.vertical, 16) | ||||
|                                 .frame(width: (geo.size.width - 66.7) / 3) | ||||
|                                 .background(Color(hex: "9970ff").opacity(0.13)) | ||||
|                                 .cornerRadius(8) | ||||
|                                 .overlay( | ||||
|                                     RoundedRectangle(cornerRadius: 8) | ||||
|                                         .stroke(Color(hex: "9970ff"), lineWidth: 1) | ||||
|                                 ) | ||||
|                                 .onTapGesture { | ||||
|                                     cancelButtonAction() | ||||
|                                 } | ||||
|                         } | ||||
|                          | ||||
|                         Text(confirmButtonTitle) | ||||
|                             .font(.custom(Font.bold.rawValue, size: 15.3)) | ||||
|                             .foregroundColor(Color(hex: "ffffff")) | ||||
|                             .padding(.vertical, 16) | ||||
|                             .frame(width: (geo.size.width - 66.7) * 2 / 3) | ||||
|                             .background(Color(hex: "9970ff")) | ||||
|                             .cornerRadius(8) | ||||
|                             .onTapGesture { | ||||
|                                 confirmButtonAction() | ||||
|                             } | ||||
|                     } | ||||
|                     .padding(.top, 45) | ||||
|                     .padding(.bottom, 16.7) | ||||
|                 } | ||||
|                 .frame(width: geo.size.width - 26.7, alignment: .center) | ||||
|                 .background(Color(hex: "222222")) | ||||
|                 .cornerRadius(10) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct SodaDialog_Previews: PreviewProvider { | ||||
|     static var previews: some View { | ||||
|         SodaDialog( | ||||
|             title: "작성글 등록", | ||||
|             desc: "작성한 글을 등록하시겠습니까?", | ||||
|             confirmButtonTitle: "결제 후 입장", | ||||
|             confirmButtonAction: {}, | ||||
|             cancelButtonTitle: "취소", | ||||
|             cancelButtonAction: {} | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -19,7 +19,7 @@ enum UserDefaultsKey: String, CaseIterable { | ||||
|     case profileImage | ||||
|     case devicePushToken | ||||
|     case isContentPlayLoop | ||||
|     case isFollowedCreatorLive | ||||
|     case isFollowedChannel | ||||
|     case isViewedOnboardingView | ||||
|     case notShowingEventPopupId | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,93 @@ | ||||
| // | ||||
| //  SectionEventBannerView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/08/09. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
| import Kingfisher | ||||
|  | ||||
| struct SectionEventBannerView: View { | ||||
|      | ||||
|     @State private var currentIndex = -1 | ||||
|     @State private var timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect() | ||||
|      | ||||
|     let items: [EventItem] | ||||
|      | ||||
|     var body: some View { | ||||
|         GeometryReader { proxy in | ||||
|             VStack(spacing: 13.3) { | ||||
|                 TabView(selection: $currentIndex) { | ||||
|                     ForEach(0..<items.count, id: \.self) { index in | ||||
|                         let item = items[index] | ||||
|                         if let url = item.thumbnailImageUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { | ||||
|                             KFImage(URL(string: url)) | ||||
|                                 .resizable() | ||||
|                                 .frame(width: proxy.size.width, height: proxy.size.height, alignment: .center) | ||||
|                                 .tag(index) | ||||
|                                 .onTapGesture { | ||||
|                                     if let _ = item.detailImageUrl { | ||||
|                                     } else if let link = item.link, link.trimmingCharacters(in: .whitespaces).count > 0, let url = URL(string: link), UIApplication.shared.canOpenURL(url) { | ||||
|                                         UIApplication.shared.open(url) | ||||
|                                     } | ||||
|                                 } | ||||
|                         } else { | ||||
|                             KFImage(URL(string: item.thumbnailImageUrl)) | ||||
|                                 .resizable() | ||||
|                                 .frame(width: proxy.size.width, height: proxy.size.height, alignment: .center) | ||||
|                                 .tag(index) | ||||
|                                 .onTapGesture { | ||||
|                                     if let _ = item.detailImageUrl { | ||||
|                                     } else if let link = item.link, link.trimmingCharacters(in: .whitespaces).count > 0, let url = URL(string: link), UIApplication.shared.canOpenURL(url) { | ||||
|                                         UIApplication.shared.open(url) | ||||
|                                     } | ||||
|                                 } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) | ||||
|                 .frame( | ||||
|                     width: proxy.size.width, | ||||
|                     height: proxy.size.height, | ||||
|                     alignment: .center | ||||
|                 ) | ||||
|                  | ||||
|                 HStack(spacing: 4) { | ||||
|                     ForEach(0..<items.count, id: \.self) { index in | ||||
|                         Capsule() | ||||
|                             .foregroundColor(index == currentIndex ? Color(hex: "9970ff") : Color(hex: "909090")) | ||||
|                             .frame( | ||||
|                                 width: index == currentIndex ? 18 : 6, | ||||
|                                 height: 6 | ||||
|                             ) | ||||
|                             .tag(index) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             .onAppear { | ||||
|                 timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect() | ||||
|             } | ||||
|             .onDisappear { | ||||
|                 timer.upstream.connect().cancel() | ||||
|             } | ||||
|             .onReceive(timer) { _ in | ||||
|                 DispatchQueue.main.async { | ||||
|                     withAnimation { | ||||
|                         if currentIndex == items.count - 1 { | ||||
|                             currentIndex = 0 | ||||
|                         } else { | ||||
|                             currentIndex += 1 | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct SectionEventBannerView_Previews: PreviewProvider { | ||||
|     static var previews: some View { | ||||
|         SectionEventBannerView(items: []) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										26
									
								
								SodaLive/Sources/Live/GetRoomListResponse.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,26 @@ | ||||
| // | ||||
| //  GetRoomListResponse.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/08/09. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
|  | ||||
| struct GetRoomListResponse: Decodable, Hashable { | ||||
|     let roomId: Int | ||||
|     let title: String | ||||
|     let content: String | ||||
|     let beginDateTime: String | ||||
|     let numberOfParticipate: Int | ||||
|     let numberOfPeople: Int | ||||
|     let coverImageUrl: String | ||||
|     let isAdult: Bool | ||||
|     let price: Int | ||||
|     let tags: [String] | ||||
|     let channelName: String? | ||||
|     let managerNickname: String | ||||
|     let managerId: Int | ||||
|     let isReservation: Bool | ||||
|     let isPrivateRoom: Bool | ||||
| } | ||||
							
								
								
									
										57
									
								
								SodaLive/Sources/Live/LiveApi.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,57 @@ | ||||
| // | ||||
| //  LiveApi.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/08/09. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
| import Moya | ||||
|  | ||||
| enum LiveApi { | ||||
|     case roomList(request: GetRoomListRequest) | ||||
| } | ||||
|  | ||||
| extension LiveApi: TargetType { | ||||
|     var baseURL: URL { | ||||
|         return URL(string: BASE_URL)! | ||||
|     } | ||||
|      | ||||
|     var path: String { | ||||
|         switch self { | ||||
|         case .roomList: | ||||
|             return "/live/room" | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var method: Moya.Method { | ||||
|         switch self { | ||||
|         case .roomList: | ||||
|             return .get | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var task: Moya.Task { | ||||
|         switch self { | ||||
|         case .roomList(let request): | ||||
|             var parameters = [ | ||||
|                 "timezone": request.timezone, | ||||
|                 "status": request.status.rawValue, | ||||
|                 "page": request.page - 1, | ||||
|                 "size": request.size, | ||||
|             ] as [String : Any] | ||||
|              | ||||
|             if let dateString = request.dateString { | ||||
|                 parameters["dateString"] = dateString | ||||
|             } | ||||
|              | ||||
|             return .requestParameters( | ||||
|                 parameters: parameters, | ||||
|                 encoding: URLEncoding.queryString) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var headers: [String : String]? { | ||||
|         return ["Authorization": "Bearer \(UserDefaults.string(forKey: UserDefaultsKey.token))"] | ||||
|     } | ||||
| } | ||||
							
								
								
									
										19
									
								
								SodaLive/Sources/Live/LiveRepository.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,19 @@ | ||||
| // | ||||
| //  LiveRepository.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/08/09. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
| import CombineMoya | ||||
| import Combine | ||||
| import Moya | ||||
|  | ||||
| final class LiveRepository { | ||||
|     private let api = MoyaProvider<LiveApi>() | ||||
|      | ||||
|     func roomList(request: GetRoomListRequest) -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.roomList(request: request)) | ||||
|     } | ||||
| } | ||||
| @@ -6,10 +6,123 @@ | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
| import RefreshableScrollView | ||||
|  | ||||
| struct LiveView: View { | ||||
|      | ||||
|     @StateObject var viewModel = LiveViewModel() | ||||
|     @StateObject var appState = AppState.shared | ||||
|      | ||||
|     var body: some View { | ||||
|         Text("Live") | ||||
|         ZStack { | ||||
|             Color.black.ignoresSafeArea() | ||||
|              | ||||
|             GeometryReader { geo in | ||||
|                 ZStack(alignment: .bottomTrailing) { | ||||
|                     RefreshableScrollView( | ||||
|                         refreshing: $viewModel.isRefresh, | ||||
|                         action: { | ||||
|                             viewModel.getSummary() | ||||
|                         } | ||||
|                     ) { | ||||
|                         VStack(spacing: 0) { | ||||
|                             if viewModel.recommendLiveItems.count > 0 { | ||||
|                                 SectionRecommendLiveView(items: viewModel.recommendLiveItems) | ||||
|                                     .padding(.top, 13.3) | ||||
|                             } | ||||
|                              | ||||
|                             if let url = URL(string: "https://blog.naver.com/yozmlive"), | ||||
|                                UIApplication.shared.canOpenURL(url) { | ||||
|                                 Image("img_how_to_use") | ||||
|                                     .resizable() | ||||
|                                     .frame( | ||||
|                                         width: screenSize().width, | ||||
|                                         height: (200 * screenSize().width) / 1080 | ||||
|                                     ) | ||||
|                                     .padding(.top, 21.3) | ||||
|                                     .onTapGesture { | ||||
|                                         UIApplication.shared.open(url) | ||||
|                                     } | ||||
|                             } | ||||
|                              | ||||
|                             if viewModel.recommendChannelItems.count > 0 { | ||||
|                                 SectionRecommendChannelView( | ||||
|                                     items: viewModel.isFollowingList ? | ||||
|                                     viewModel.followedChannelItems : | ||||
|                                         viewModel.recommendChannelItems, | ||||
|                                     isFollowingList: $viewModel.isFollowingList | ||||
|                                 ) | ||||
|                                 .padding(.top, 40) | ||||
|                             } | ||||
|                              | ||||
|                             if viewModel.liveNowItems.count > 0 { | ||||
|                                 SectionLiveNowView( | ||||
|                                     items: viewModel.liveNowItems, | ||||
|                                     onClickParticipant: {_ in}, | ||||
|                                     onTapCreateLive: {} | ||||
|                                 ) | ||||
|                                 .padding(.top, 40) | ||||
|                             } | ||||
|                              | ||||
|                             if viewModel.eventBannerItems.count > 0 { | ||||
|                                 SectionEventBannerView(items: viewModel.eventBannerItems) | ||||
|                                     .frame( | ||||
|                                         width: viewModel.eventBannerItems.count > 0 ? screenSize().width : 0, | ||||
|                                         height: viewModel.eventBannerItems.count > 0 ? screenSize().width * 300 / 1000 : 0, | ||||
|                                         alignment: .center | ||||
|                                     ) | ||||
|                                     .padding(.vertical, 40) | ||||
|                             } | ||||
|                              | ||||
|                             if viewModel.liveReservationItems.count > 0 { | ||||
|                                 SectionLiveReservationView( | ||||
|                                     items: viewModel.liveReservationItems, | ||||
|                                     onClickCancel: { viewModel.getSummary() }, | ||||
|                                     onClickStart: {_ in}, | ||||
|                                     onClickReservation: {_ in}, | ||||
|                                     onTapCreateLive: {} | ||||
|                                 ) | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                     .frame(width: geo.size.width, height: geo.size.height) | ||||
|                     .onAppear { | ||||
|                         viewModel.getSummary() | ||||
|                     } | ||||
|                      | ||||
|                     Image("btn_make_live") | ||||
|                         .padding(.trailing, 16) | ||||
|                         .padding(.bottom, 16) | ||||
|                         .onTapGesture {} | ||||
|                 } | ||||
|             } | ||||
|              | ||||
|             if viewModel.isShowPaymentDialog { | ||||
|                 SodaDialog( | ||||
|                     title: viewModel.paymentDialogTitle, | ||||
|                     desc: viewModel.paymentDialogDesc, | ||||
|                     confirmButtonTitle: viewModel.paymentDialogConfirmTitle, | ||||
|                     confirmButtonAction: viewModel.paymentDialogConfirmAction, | ||||
|                     cancelButtonTitle: viewModel.paymentDialogCancelTitle, | ||||
|                     cancelButtonAction: viewModel.hidePopup | ||||
|                 ) | ||||
|             } | ||||
|              | ||||
|             if viewModel.isShowPasswordDialog { | ||||
|                 LiveRoomPasswordDialog( | ||||
|                     isShowing: $viewModel.isShowPasswordDialog, | ||||
|                     can: viewModel.secretOrPasswordDialogCoin, | ||||
|                     confirmAction: viewModel.passwordDialogConfirmAction | ||||
|                 ) | ||||
|             } | ||||
|              | ||||
|             if viewModel.isFollowedChannelLoading || | ||||
|                 viewModel.isRecommendChannelLoading || | ||||
|                 viewModel.isRecommendLiveLoading || | ||||
|                 viewModel.isLoading { | ||||
|                 LoadingView() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
							
								
								
									
										292
									
								
								SodaLive/Sources/Live/LiveViewModel.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,292 @@ | ||||
| // | ||||
| //  LiveViewModel.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/08/09. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
| import Combine | ||||
|  | ||||
| final class LiveViewModel: ObservableObject { | ||||
|      | ||||
|     private let repository = LiveRepository() | ||||
|     private let eventRepository = EventRepository() | ||||
|     private let liveRecommendRepository = LiveRecommendRepository() | ||||
|     private var subscription = Set<AnyCancellable>() | ||||
|      | ||||
|     @Published private(set) var eventBannerItems = [EventItem]() | ||||
|     @Published private(set) var liveNowItems = [GetRoomListResponse]() | ||||
|     @Published private(set) var liveReservationItems = [GetRoomListResponse]() | ||||
|     @Published private(set) var recommendLiveItems: [GetRecommendLiveResponse] = [] | ||||
|     @Published private(set) var recommendChannelItems: [GetRecommendChannelResponse] = [] | ||||
|     @Published private(set) var followedChannelItems: [GetRecommendChannelResponse] = [] | ||||
|      | ||||
|     @Published var errorMessage = "" | ||||
|     @Published var isShowPopup = false | ||||
|     @Published var isRefresh = false | ||||
|     @Published var isLoading = false | ||||
|     @Published var isRecommendLiveLoading = false | ||||
|     @Published var isRecommendChannelLoading = false | ||||
|     @Published var isFollowedChannelLoading = false | ||||
|      | ||||
|     @Published var paymentDialogTitle = "" | ||||
|     @Published var paymentDialogDesc = "" | ||||
|     @Published var isShowPaymentDialog = false | ||||
|     @Published var paymentDialogConfirmAction = {} | ||||
|     @Published var paymentDialogConfirmTitle = "" | ||||
|      | ||||
|     @Published var secretOrPasswordDialogCoin = 0 | ||||
|     @Published var passwordDialogConfirmAction: (Int) -> Void = { _ in } | ||||
|     @Published var isShowPasswordDialog = false | ||||
|      | ||||
|     @Published var isFollowingList = UserDefaults.bool(forKey: .isFollowedChannel) { | ||||
|         didSet { | ||||
|             UserDefaults.set(isFollowingList, forKey: .isFollowedChannel) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     let paymentDialogCancelTitle = "취소" | ||||
|      | ||||
|     var page = 1 | ||||
|     var isLast = false | ||||
|     private let pageSize = 10 | ||||
|      | ||||
|     func hidePopup() { | ||||
|         isShowPaymentDialog = false | ||||
|         isShowPasswordDialog = false | ||||
|          | ||||
|         paymentDialogTitle = "" | ||||
|         paymentDialogDesc = "" | ||||
|         paymentDialogConfirmAction = {} | ||||
|          | ||||
|         secretOrPasswordDialogCoin = 0 | ||||
|          | ||||
|         passwordDialogConfirmAction = { _ in } | ||||
|     } | ||||
|      | ||||
|     func getSummary() { | ||||
|         getFollowedChannelList() | ||||
|         getRecommendChannelList() | ||||
|         getRecommendLive() | ||||
|         isLoading = true | ||||
|          | ||||
|         eventBannerItems.removeAll() | ||||
|         liveNowItems.removeAll() | ||||
|         liveReservationItems.removeAll() | ||||
|          | ||||
|         let liveNow = repository.roomList( | ||||
|             request: GetRoomListRequest( | ||||
|                 timezone: TimeZone.current.identifier, | ||||
|                 dateString: nil, | ||||
|                 status: .NOW, | ||||
|                 page: 1, | ||||
|                 size: 10 | ||||
|             ) | ||||
|         ) | ||||
|          | ||||
|         let liveReservation = repository.roomList( | ||||
|             request: GetRoomListRequest( | ||||
|                 timezone: TimeZone.current.identifier, | ||||
|                 dateString: nil, | ||||
|                 status: .RESERVATION, | ||||
|                 page: 1, | ||||
|                 size: 10 | ||||
|             ) | ||||
|         ) | ||||
|          | ||||
|         let event = eventRepository.getEvents() | ||||
|          | ||||
|         Publishers | ||||
|             .CombineLatest3(liveNow, liveReservation, event) | ||||
|             .sink { result in | ||||
|                 switch result { | ||||
|                 case .finished: | ||||
|                     DEBUG_LOG("finish") | ||||
|                 case .failure(let error): | ||||
|                     ERROR_LOG(error.localizedDescription) | ||||
|                 } | ||||
|             } receiveValue: { (now, reservation, eventResponse) in | ||||
|                 let nowData = now.data | ||||
|                 let reservationData = reservation.data | ||||
|                 let eventData = eventResponse.data | ||||
|                  | ||||
|                 let jsonDecoder = JSONDecoder() | ||||
|                  | ||||
|                 do { | ||||
|                     let nowDecoded = try jsonDecoder.decode(ApiResponse<[GetRoomListResponse]>.self, from: nowData) | ||||
|                     if let data = nowDecoded.data, nowDecoded.success { | ||||
|                         self.liveNowItems.removeAll() | ||||
|                         self.liveNowItems.append(contentsOf: data) | ||||
|                     } else { | ||||
|                         if let message = nowDecoded.message { | ||||
|                             self.errorMessage = message | ||||
|                         } else { | ||||
|                             self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." | ||||
|                         } | ||||
|                          | ||||
|                         self.isShowPopup = true | ||||
|                     } | ||||
|                 } catch { | ||||
|                     self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." | ||||
|                     self.isShowPopup = true | ||||
|                 } | ||||
|                  | ||||
|                 do { | ||||
|                     let reservationDecoded = try jsonDecoder.decode(ApiResponse<[GetRoomListResponse]>.self, from: reservationData) | ||||
|                     if let data = reservationDecoded.data, reservationDecoded.success { | ||||
|                         self.liveReservationItems.removeAll() | ||||
|                         self.liveReservationItems.append(contentsOf: data) | ||||
|                     } else { | ||||
|                         if let message = reservationDecoded.message { | ||||
|                             self.errorMessage = message | ||||
|                         } else { | ||||
|                             self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." | ||||
|                         } | ||||
|                          | ||||
|                         self.isShowPopup = true | ||||
|                     } | ||||
|                 } catch { | ||||
|                     self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." | ||||
|                     self.isShowPopup = true | ||||
|                 } | ||||
|                  | ||||
|                 do { | ||||
|                     let eventDecoded = try jsonDecoder.decode(ApiResponse<GetEventResponse>.self, from: eventData) | ||||
|                     if let data = eventDecoded.data, eventDecoded.success { | ||||
|                         self.eventBannerItems.removeAll() | ||||
|                         self.eventBannerItems.append(contentsOf: data.eventList) | ||||
|                     } else { | ||||
|                         if let message = eventDecoded.message { | ||||
|                             self.errorMessage = message | ||||
|                         } else { | ||||
|                             self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." | ||||
|                         } | ||||
|                          | ||||
|                         self.isShowPopup = true | ||||
|                     } | ||||
|                 } catch { | ||||
|                     self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." | ||||
|                     self.isShowPopup = true | ||||
|                 } | ||||
|                  | ||||
|                 self.isLoading = false | ||||
|                 self.isRefresh = false | ||||
|             } | ||||
|             .store(in: &subscription) | ||||
|     } | ||||
|      | ||||
|     private func getFollowedChannelList() { | ||||
|         followedChannelItems.removeAll() | ||||
|         isFollowedChannelLoading = true | ||||
|          | ||||
|         liveRecommendRepository.getFollowedChannelList() | ||||
|             .sink { result in | ||||
|                 switch result { | ||||
|                 case .finished: | ||||
|                     DEBUG_LOG("finish") | ||||
|                 case .failure(let error): | ||||
|                     ERROR_LOG(error.localizedDescription) | ||||
|                 } | ||||
|             } receiveValue: { [unowned self] response in | ||||
|                 self.isFollowedChannelLoading = false | ||||
|                 let responseData = response.data | ||||
|                  | ||||
|                 do { | ||||
|                     let jsonDecoder = JSONDecoder() | ||||
|                     let decoded = try jsonDecoder.decode(ApiResponse<[GetRecommendChannelResponse]>.self, from: responseData) | ||||
|                      | ||||
|                     if let data = decoded.data, decoded.success { | ||||
|                         self.followedChannelItems.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) | ||||
|     } | ||||
|      | ||||
|     private func getRecommendChannelList() { | ||||
|         recommendChannelItems.removeAll() | ||||
|         isRecommendChannelLoading = true | ||||
|          | ||||
|         liveRecommendRepository.getRecommendChannelList() | ||||
|             .sink { result in | ||||
|                 switch result { | ||||
|                 case .finished: | ||||
|                     DEBUG_LOG("finish") | ||||
|                 case .failure(let error): | ||||
|                     ERROR_LOG(error.localizedDescription) | ||||
|                 } | ||||
|             } receiveValue: { [unowned self] response in | ||||
|                 self.isRecommendChannelLoading = false | ||||
|                 let responseData = response.data | ||||
|                  | ||||
|                 do { | ||||
|                     let jsonDecoder = JSONDecoder() | ||||
|                     let decoded = try jsonDecoder.decode(ApiResponse<[GetRecommendChannelResponse]>.self, from: responseData) | ||||
|                      | ||||
|                     if let data = decoded.data, decoded.success { | ||||
|                         self.recommendChannelItems.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) | ||||
|     } | ||||
|      | ||||
|     private func getRecommendLive() { | ||||
|         recommendLiveItems.removeAll() | ||||
|         isRecommendLiveLoading = true | ||||
|          | ||||
|         liveRecommendRepository.getRecommendLive() | ||||
|             .sink { result in | ||||
|                 switch result { | ||||
|                 case .finished: | ||||
|                     DEBUG_LOG("finish") | ||||
|                 case .failure(let error): | ||||
|                     ERROR_LOG(error.localizedDescription) | ||||
|                 } | ||||
|             } receiveValue: { [unowned self] response in | ||||
|                 self.isRecommendLiveLoading = false | ||||
|                 let responseData = response.data | ||||
|                  | ||||
|                 do { | ||||
|                     let jsonDecoder = JSONDecoder() | ||||
|                     let decoded = try jsonDecoder.decode(ApiResponse<[GetRecommendLiveResponse]>.self, from: responseData) | ||||
|                      | ||||
|                     if let data = decoded.data, decoded.success { | ||||
|                         self.recommendLiveItems.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) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										31
									
								
								SodaLive/Sources/Live/Now/SectionLiveNowView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,31 @@ | ||||
| // | ||||
| //  SectionLiveNowView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/08/09. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct SectionLiveNowView: View { | ||||
|      | ||||
|     let items: [GetRoomListResponse] | ||||
|      | ||||
|     let onClickParticipant: (Int) -> Void | ||||
|     let onTapCreateLive: () -> Void | ||||
|      | ||||
|     var body: some View { | ||||
|         VStack(spacing: 13.3) { | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct SectionLiveNowView_Previews: PreviewProvider { | ||||
|     static var previews: some View { | ||||
|         SectionLiveNowView( | ||||
|             items: [], | ||||
|             onClickParticipant: { _ in }, | ||||
|             onTapCreateLive: {} | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,13 @@ | ||||
| // | ||||
| //  GetRecommendLiveResponse.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/08/09. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
|  | ||||
| struct GetRecommendLiveResponse: Decodable, Hashable { | ||||
|     let imageUrl: String | ||||
|     let creatorId: Int | ||||
| } | ||||
							
								
								
									
										56
									
								
								SodaLive/Sources/Live/Recommend/LiveRecommendApi.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,56 @@ | ||||
| // | ||||
| //  LiveRecommendApi.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/08/09. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
| import Moya | ||||
|  | ||||
| enum LiveRecommendApi { | ||||
|     case getFollowedChannelList | ||||
|     case getRecommendChannelList | ||||
|     case getRecommendLive | ||||
| } | ||||
|  | ||||
| extension LiveRecommendApi: TargetType { | ||||
|     var baseURL: URL { | ||||
|         return URL(string: BASE_URL)! | ||||
|     } | ||||
|      | ||||
|     var path: String { | ||||
|         switch self { | ||||
|         case .getFollowedChannelList: | ||||
|             return "/live/recommend/following/channel/list" | ||||
|              | ||||
|         case .getRecommendChannelList: | ||||
|             return "/live/recommend/channel" | ||||
|              | ||||
|         case .getRecommendLive: | ||||
|             return "/live/recommend" | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var method: Moya.Method { | ||||
|         switch self { | ||||
|         case .getFollowedChannelList, | ||||
|                 .getRecommendChannelList, | ||||
|                 .getRecommendLive: | ||||
|             return .get | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var task: Moya.Task { | ||||
|         switch self { | ||||
|         case .getFollowedChannelList, | ||||
|                 .getRecommendChannelList, | ||||
|                 .getRecommendLive: | ||||
|             return .requestPlain | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var headers: [String : String]? { | ||||
|         return ["Authorization": "Bearer \(UserDefaults.string(forKey: UserDefaultsKey.token))"] | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,27 @@ | ||||
| // | ||||
| //  LiveRecommendRepository.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/08/09. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
| import CombineMoya | ||||
| import Combine | ||||
| import Moya | ||||
|  | ||||
| final class LiveRecommendRepository { | ||||
|     private let api = MoyaProvider<LiveRecommendApi>() | ||||
|      | ||||
|     func getFollowedChannelList() -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.getFollowedChannelList) | ||||
|     } | ||||
|      | ||||
|     func getRecommendChannelList() -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.getRecommendChannelList) | ||||
|     } | ||||
|      | ||||
|     func getRecommendLive() -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.getRecommendLive) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										114
									
								
								SodaLive/Sources/Live/Recommend/SectionRecommendLiveView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,114 @@ | ||||
| // | ||||
| //  SectionRecommendLiveView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/08/09. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
| import Kingfisher | ||||
|  | ||||
| struct SectionRecommendLiveView: View { | ||||
|      | ||||
|     let items: [GetRecommendLiveResponse] | ||||
|     @State private var currentIndex = 0 | ||||
|     @State private var timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect() | ||||
|      | ||||
|     var body: some View { | ||||
|         VStack(spacing: 0) { | ||||
|             HStack(spacing: 0) { | ||||
|                 Text("추천 ") | ||||
|                     .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                     .foregroundColor(Color(hex: "ff5c49")) | ||||
|                  | ||||
|                 Text("라이브") | ||||
|                     .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                     .foregroundColor(Color(hex: "eeeeee")) | ||||
|             } | ||||
|             .frame(width: screenSize().width - 26.7, alignment: .leading) | ||||
|              | ||||
|             TabView(selection: $currentIndex) { | ||||
|                 ForEach(0..<items.count, id: \.self) { index in | ||||
|                     let item = items[index] | ||||
|                     if let url = item.imageUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { | ||||
|                         KFImage(URL(string: url)) | ||||
|                             .resizable() | ||||
|                             .scaledToFill() | ||||
|                             .frame( | ||||
|                                 width: screenSize().width - 26.7, | ||||
|                                 height: (screenSize().width - 26.7) * 0.53 | ||||
|                             ) | ||||
|                             .onTapGesture { | ||||
|                             } | ||||
|                             .cornerRadius(4.7) | ||||
|                     } else { | ||||
|                         KFImage(URL(string: item.imageUrl)) | ||||
|                             .resizable() | ||||
|                             .scaledToFill() | ||||
|                             .frame( | ||||
|                                 width: screenSize().width - 26.7, | ||||
|                                 height: (screenSize().width - 26.7) * 0.53 | ||||
|                             ) | ||||
|                             .onTapGesture { | ||||
|                             } | ||||
|                             .cornerRadius(4.7) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) | ||||
|             .frame( | ||||
|                 width: screenSize().width - 26.7, | ||||
|                 height: (screenSize().width - 26.7) * 0.53 | ||||
|             ) | ||||
|             .padding(.top, 26.7) | ||||
|              | ||||
|             HStack(spacing: 4) { | ||||
|                 ForEach(0..<items.count, id: \.self) { index in | ||||
|                     Capsule() | ||||
|                         .foregroundColor(index == currentIndex ? Color(hex: "9970ff") : Color(hex: "909090")) | ||||
|                         .frame( | ||||
|                             width: index == currentIndex ? 18 : 6, | ||||
|                             height: 6 | ||||
|                         ) | ||||
|                         .tag(index) | ||||
|                 } | ||||
|             } | ||||
|             .padding(.top, 13.3) | ||||
|         } | ||||
|         .frame(width: screenSize().width - 26.7) | ||||
|         .onAppear { | ||||
|             timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect() | ||||
|         } | ||||
|         .onDisappear { | ||||
|             timer.upstream.connect().cancel() | ||||
|         } | ||||
|         .onReceive(timer) { _ in | ||||
|             DispatchQueue.main.async { | ||||
|                 withAnimation { | ||||
|                     if currentIndex == items.count - 1 { | ||||
|                         currentIndex = 0 | ||||
|                     } else { | ||||
|                         currentIndex += 1 | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct SectionRecommendLiveView_Previews: PreviewProvider { | ||||
|     static var previews: some View { | ||||
|         SectionRecommendLiveView( | ||||
|             items: [ | ||||
|                 GetRecommendLiveResponse( | ||||
|                     imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                     creatorId: 1 | ||||
|                 ), | ||||
|                 GetRecommendLiveResponse( | ||||
|                     imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                     creatorId: 2 | ||||
|                 ) | ||||
|             ] | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,15 @@ | ||||
| // | ||||
| //  GetRecommendChannelResponse.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/08/09. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
|  | ||||
| struct GetRecommendChannelResponse: Decodable, Hashable { | ||||
|     let creatorId: Int | ||||
|     let nickname: String | ||||
|     let profileImageUrl: String | ||||
|     let isOnAir: Bool | ||||
| } | ||||
| @@ -0,0 +1,124 @@ | ||||
| // | ||||
| //  SectionRecommendChannelView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/08/09. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
| import Kingfisher | ||||
|  | ||||
| struct SectionRecommendChannelView: View { | ||||
|      | ||||
|     let items: [GetRecommendChannelResponse] | ||||
|      | ||||
|     @Binding var isFollowingList: Bool | ||||
|      | ||||
|     var body: some View { | ||||
|         VStack(spacing: 21.3) { | ||||
|             HStack(spacing: 0) { | ||||
|                 if isFollowingList { | ||||
|                     Text("팔로잉 ") | ||||
|                         .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                         .foregroundColor(Color(hex: "eeeeee")) | ||||
|                 } else { | ||||
|                     Text("추천 ") | ||||
|                         .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                         .foregroundColor(Color(hex: "eeeeee")) | ||||
|                 } | ||||
|                  | ||||
|                 Text("채널") | ||||
|                     .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                     .foregroundColor(Color(hex: "9970ff")) | ||||
|                  | ||||
|                 Spacer() | ||||
|                  | ||||
|                 Text("팔로잉 채널") | ||||
|                     .font(.custom(Font.medium.rawValue, size: 13.3)) | ||||
|                     .foregroundColor(Color(hex: "777777")) | ||||
|                  | ||||
|                 Image(isFollowingList ? "btn_toggle_on_big" : "btn_toggle_off_big") | ||||
|                     .resizable() | ||||
|                     .frame(width: 33.3, height: 20) | ||||
|                     .padding(.leading, 6.7) | ||||
|                     .onTapGesture { | ||||
|                         isFollowingList.toggle() | ||||
|                     } | ||||
|             } | ||||
|             .frame(width: screenSize().width - 26.7, alignment: .leading) | ||||
|              | ||||
|             ScrollView(.horizontal, showsIndicators: false) { | ||||
|                 HStack(spacing: 21.3) { | ||||
|                     ForEach(0..<items.count, id: \.self) { index in | ||||
|                         let item = items[index] | ||||
|                          | ||||
|                         VStack(spacing: 13.3) { | ||||
|                             ZStack(alignment: .bottom) { | ||||
|                                 KFImage(URL(string: item.profileImageUrl)) | ||||
|                                     .resizable() | ||||
|                                     .scaledToFill() | ||||
|                                     .frame(width: screenSize().width * 0.18, height: screenSize().width * 0.18, alignment: .center) | ||||
|                                     .cornerRadius(screenSize().width * 0.09) | ||||
|                                     .overlay( | ||||
|                                         Circle() | ||||
|                                             .strokeBorder(lineWidth: 3) | ||||
|                                             .foregroundColor( | ||||
|                                                 Color(hex: "9970ff") | ||||
|                                                     .opacity(item.isOnAir ? 1 : 0) | ||||
|                                             ) | ||||
|                                     ) | ||||
|                                  | ||||
|                                 if item.isOnAir { | ||||
|                                     Text("Live") | ||||
|                                         .font(.custom(Font.bold.rawValue, size: 8.7)) | ||||
|                                         .foregroundColor(.white) | ||||
|                                         .padding(.vertical, 2.7) | ||||
|                                         .padding(.horizontal, 5.7) | ||||
|                                         .background(Color(hex: "9970ff")) | ||||
|                                         .cornerRadius(6.7) | ||||
|                                 } | ||||
|                             } | ||||
|                              | ||||
|                             Text(item.nickname) | ||||
|                                 .font(.custom(Font.medium.rawValue, size: 11.3)) | ||||
|                                 .foregroundColor(Color(hex: "bbbbbb")) | ||||
|                                 .frame(width: screenSize().width * 0.18) | ||||
|                                 .lineLimit(1) | ||||
|                         } | ||||
|                         .onTapGesture { | ||||
|                         } | ||||
|                     } | ||||
|                      | ||||
|                     if isFollowingList { | ||||
|                         VStack(spacing: 10.7) { | ||||
|                             Image("btn_item_more") | ||||
|                                 .resizable() | ||||
|                                 .frame(width: screenSize().width * 0.18, height: screenSize().width * 0.18, alignment: .center) | ||||
|                              | ||||
|                             Text("더보기") | ||||
|                                 .font(.custom(Font.medium.rawValue, size: 11.3)) | ||||
|                                 .foregroundColor(Color(hex: "bbbbbb")) | ||||
|                                 .frame(width: screenSize().width * 0.18) | ||||
|                                 .lineLimit(1) | ||||
|                         } | ||||
|                         .onTapGesture { | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct SectionRecommendChannelView_Previews: PreviewProvider { | ||||
|     static var previews: some View { | ||||
|         SectionRecommendChannelView( | ||||
|             items: [ | ||||
|                 GetRecommendChannelResponse(creatorId: 1, nickname: "크리에이터1", profileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", isOnAir: true), | ||||
|                 GetRecommendChannelResponse(creatorId: 2, nickname: "크리에이터2", profileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", isOnAir: false), | ||||
|                 GetRecommendChannelResponse(creatorId: 3, nickname: "크리에이터3", profileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", isOnAir: false) | ||||
|             ], | ||||
|             isFollowingList: .constant(true) | ||||
|         ) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										123
									
								
								SodaLive/Sources/Live/Reservation/LiveReservationItemView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,123 @@ | ||||
| // | ||||
| //  LiveReservationItemView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/08/10. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
| import Kingfisher | ||||
|  | ||||
| struct LiveReservationItemView: View { | ||||
|      | ||||
|     let item: GetRoomListResponse | ||||
|      | ||||
|     var body: some View { | ||||
|         VStack(alignment: .leading, spacing: 13.3) { | ||||
|             HStack(spacing: 20) { | ||||
|                 ZStack(alignment: .topLeading) { | ||||
|                     KFImage(URL(string: item.coverImageUrl)) | ||||
|                         .resizable() | ||||
|                         .scaledToFill() | ||||
|                         .frame(width: 80, height: 116, alignment: .top) | ||||
|                         .cornerRadius(4.7) | ||||
|                         .clipped() | ||||
|                      | ||||
|                     if item.isAdult { | ||||
|                         Text("19") | ||||
|                             .font(.custom(Font.bold.rawValue, size: 11.3)) | ||||
|                             .foregroundColor(Color.white) | ||||
|                             .padding(4) | ||||
|                             .background(Color(hex: "e53621")) | ||||
|                             .cornerRadius(20) | ||||
|                             .padding(.top, 3.3) | ||||
|                             .padding(.leading, 3.3) | ||||
|                     } | ||||
|                 } | ||||
|                  | ||||
|                 HStack(alignment: .top, spacing: 0) { | ||||
|                     VStack(alignment: .leading, spacing: 0) { | ||||
|                         Text(item.beginDateTime) | ||||
|                             .font(.custom(Font.medium.rawValue, size: 9.3)) | ||||
|                             .foregroundColor(Color(hex: "ffd300")) | ||||
|                          | ||||
|                         Text(item.managerNickname) | ||||
|                             .font(.custom(Font.medium.rawValue, size: 11.3)) | ||||
|                             .foregroundColor(Color(hex: "bbbbbb")) | ||||
|                             .padding(.top, 10) | ||||
|                          | ||||
|                         Text(item.title) | ||||
|                             .font(.custom(Font.medium.rawValue, size: 15.3)) | ||||
|                             .foregroundColor(Color(hex: "e2e2e2")) | ||||
|                             .padding(.top, 4.3) | ||||
|                          | ||||
|                         Spacer() | ||||
|                          | ||||
|                         if item.isReservation { | ||||
|                             Text("예약완료") | ||||
|                                 .font(.custom(Font.medium.rawValue, size: 11.3)) | ||||
|                                 .foregroundColor(Color(hex: "d2d2d2")) | ||||
|                                 .padding(.horizontal, 7) | ||||
|                                 .padding(.vertical, 4) | ||||
|                                 .background(Color(hex: "533d89")) | ||||
|                                 .cornerRadius(10) | ||||
|                         } else { | ||||
|                             if item.price > 0 { | ||||
|                                 Text("\(item.price)코인") | ||||
|                                     .font(.custom(Font.medium.rawValue, size: 12)) | ||||
|                                     .foregroundColor( | ||||
|                                         Color(hex: "e2e2e2") | ||||
|                                             .opacity(0.5) | ||||
|                                     ) | ||||
|                                  | ||||
|                             } else { | ||||
|                                 Text("무료") | ||||
|                                     .font(.custom(Font.medium.rawValue, size: 12)) | ||||
|                                     .foregroundColor( | ||||
|                                         Color(hex: "e2e2e2") | ||||
|                                             .opacity(0.5) | ||||
|                                     ) | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                      | ||||
|                     Spacer() | ||||
|                      | ||||
|                     if item.isPrivateRoom { | ||||
|                         Image("ic_lock") | ||||
|                             .resizable() | ||||
|                             .frame(width: 20, height: 20) | ||||
|                     } | ||||
|                 } | ||||
|                 .padding(.vertical, 6.7) | ||||
|             } | ||||
|              | ||||
|             Divider() | ||||
|                 .frame(height: 1) | ||||
|                 .background(Color(hex: "909090").opacity(0.5)) | ||||
|         } | ||||
|         .frame(width: screenSize().width - 26.7, height: 130, alignment: .center) | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct LiveReservationItemView_Previews: PreviewProvider { | ||||
|     static var previews: some View { | ||||
|         LiveReservationItemView(item: GetRoomListResponse( | ||||
|             roomId: 99, | ||||
|             title: "test", | ||||
|             content: "testtest", | ||||
|             beginDateTime: "2022.05.23 Mon 03:00 PM", | ||||
|             numberOfParticipate: 0, | ||||
|             numberOfPeople: 5, | ||||
|             coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|             isAdult: false, | ||||
|             price: 0, | ||||
|             tags: ["팬미팅", "힐링"], | ||||
|             channelName: nil, | ||||
|             managerNickname: "user8", | ||||
|             managerId: 19, | ||||
|             isReservation: false, | ||||
|             isPrivateRoom: true | ||||
|         )) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,114 @@ | ||||
| // | ||||
| //  MyLiveReservationItemView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/08/10. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
| import Kingfisher | ||||
|  | ||||
| struct MyLiveReservationItemView: View { | ||||
|      | ||||
|     let item: GetRoomListResponse | ||||
|     let index: Int | ||||
|      | ||||
|     var body: some View { | ||||
|         VStack(alignment: .leading, spacing: 8) { | ||||
|             if index == 0 { | ||||
|                 HStack(spacing: 8) { | ||||
|                     Image("ic_mic_colored") | ||||
|                      | ||||
|                     Text("내가 개설한 라이브") | ||||
|                         .font(.custom(Font.bold.rawValue, size: 16)) | ||||
|                         .foregroundColor(Color(hex: "9970ff")) | ||||
|                 } | ||||
|             } | ||||
|              | ||||
|             VStack(alignment: .leading, spacing: 13.3) { | ||||
|                 HStack(alignment: .top, spacing: 20) { | ||||
|                     ZStack(alignment: .topLeading) { | ||||
|                         KFImage(URL(string: item.coverImageUrl)) | ||||
|                             .resizable() | ||||
|                             .scaledToFill() | ||||
|                             .frame(width: 80, height: 116, alignment: .top) | ||||
|                             .cornerRadius(4.7) | ||||
|                             .clipped() | ||||
|                          | ||||
|                         if item.isAdult { | ||||
|                             Text("19") | ||||
|                                 .font(.custom(Font.bold.rawValue, size: 11.3)) | ||||
|                                 .foregroundColor(Color.white) | ||||
|                                 .padding(4) | ||||
|                                 .background(Color(hex: "e53621")) | ||||
|                                 .cornerRadius(20) | ||||
|                                 .padding(.top, 3.3) | ||||
|                                 .padding(.leading, 3.3) | ||||
|                         } | ||||
|                     } | ||||
|                      | ||||
|                     HStack(alignment: .center, spacing: 0) { | ||||
|                         VStack(alignment: .leading, spacing: 0) { | ||||
|                             Text(item.beginDateTime) | ||||
|                                 .font(.custom(Font.medium.rawValue, size: 9.3)) | ||||
|                                 .foregroundColor(Color(hex: "ffd300")) | ||||
|                              | ||||
|                             Text(item.managerNickname) | ||||
|                                 .font(.custom(Font.medium.rawValue, size: 11.3)) | ||||
|                                 .foregroundColor(Color(hex: "bbbbbb")) | ||||
|                                 .padding(.top, 10) | ||||
|                              | ||||
|                             Text(item.title) | ||||
|                                 .font(.custom(Font.medium.rawValue, size: 15.3)) | ||||
|                                 .foregroundColor(Color(hex: "e2e2e2")) | ||||
|                                 .padding(.top, 4.3) | ||||
|                         } | ||||
|                     }.frame(height: 116) | ||||
|                      | ||||
|                     Spacer() | ||||
|                      | ||||
|                     if item.isPrivateRoom { | ||||
|                         Image("ic_lock") | ||||
|                             .resizable() | ||||
|                             .frame(width: 20, height: 20) | ||||
|                             .padding(.trailing, 13.3) | ||||
|                             .padding(.top, 13.3) | ||||
|                     } | ||||
|                 } | ||||
|                 .overlay( | ||||
|                     RoundedRectangle(cornerRadius: 8) | ||||
|                         .stroke(Color(hex: "9970ff"), lineWidth: 1) | ||||
|                 ) | ||||
|                  | ||||
|                 Divider() | ||||
|                     .frame(height: 1) | ||||
|                     .background(Color(hex: "909090").opacity(0.5)) | ||||
|             }.frame(width: screenSize().width - 26.7) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct MyLiveReservationItemView_Previews: PreviewProvider { | ||||
|     static var previews: some View { | ||||
|         MyLiveReservationItemView( | ||||
|             item: GetRoomListResponse( | ||||
|                 roomId: 99, | ||||
|                 title: "test", | ||||
|                 content: "testtest", | ||||
|                 beginDateTime: "2022.05.23 Mon 03:00 PM", | ||||
|                 numberOfParticipate: 0, | ||||
|                 numberOfPeople: 5, | ||||
|                 coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 isAdult: false, | ||||
|                 price: 0, | ||||
|                 tags: ["팬미팅", "힐링"], | ||||
|                 channelName: nil, | ||||
|                 managerNickname: "user8", | ||||
|                 managerId: 19, | ||||
|                 isReservation: false, | ||||
|                 isPrivateRoom: true | ||||
|             ), | ||||
|             index: 0 | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,109 @@ | ||||
| // | ||||
| //  SectionLiveReservationView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/08/09. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct SectionLiveReservationView: View { | ||||
|      | ||||
|     let items: [GetRoomListResponse] | ||||
|      | ||||
|     let onClickCancel: () -> Void | ||||
|     let onClickStart: (Int) -> Void | ||||
|     let onClickReservation: (Int) -> Void | ||||
|     let onTapCreateLive: () -> Void | ||||
|      | ||||
|     var body: some View { | ||||
|         VStack(spacing: 13.3) { | ||||
|             HStack(spacing: 0) { | ||||
|                 Text("지금 ") | ||||
|                     .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                     .foregroundColor(Color(hex: "eeeeee")) | ||||
|                  | ||||
|                 Text("예약중") | ||||
|                     .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                     .foregroundColor(Color(hex: "9970ff")) | ||||
|                  | ||||
|                 Spacer() | ||||
|                  | ||||
|                 if items.count > 0 { | ||||
|                     Text("전체보기") | ||||
|                         .font(.custom(Font.light.rawValue, size: 11.3)) | ||||
|                         .foregroundColor(Color(hex: "bbbbbb")) | ||||
|                         .onTapGesture {} | ||||
|                 } | ||||
|             } | ||||
|             .padding(.horizontal, 13.3) | ||||
|             .frame(width: screenSize().width) | ||||
|              | ||||
|             if items.count > 0 { | ||||
|                 VStack(spacing: 13.3) { | ||||
|                     ForEach(0..<items.count, id: \.self) { index in | ||||
|                         let item = items[index] | ||||
|                          | ||||
|                         if item.managerId == UserDefaults.int(forKey: .userId) { | ||||
|                             MyLiveReservationItemView(item: item, index: index) | ||||
|                                 .contentShape(Rectangle()) | ||||
|                                 .onTapGesture {} | ||||
|                         } else { | ||||
|                             LiveReservationItemView(item: item) | ||||
|                                 .contentShape(Rectangle()) | ||||
|                                 .onTapGesture {} | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 .padding(.horizontal, 13.3) | ||||
|                 .frame(width: screenSize().width) | ||||
|                 .padding(.top, 28.3) | ||||
|             } else { | ||||
|                 VStack(spacing: 0) { | ||||
|                     Image("ic_no_item") | ||||
|                         .resizable() | ||||
|                         .frame(width: 60, height: 60) | ||||
|                      | ||||
|                     Text("지금 예약중인 라이브가 없습니다.\n직접 라이브를 만들어 보세요!") | ||||
|                         .font(.custom(Font.medium.rawValue, size: 10.7)) | ||||
|                         .foregroundColor(Color(hex: "bbbbbb")) | ||||
|                         .fixedSize(horizontal: false, vertical: true) | ||||
|                         .multilineTextAlignment(.center) | ||||
|                         .padding(.top, 8) | ||||
|                      | ||||
|                     HStack(spacing: 0) { | ||||
|                         Image("ic_plus_no_bg") | ||||
|                             .resizable() | ||||
|                             .frame(width: 33.3, height: 33.3, alignment: .center) | ||||
|                          | ||||
|                         Text("라이브 만들기") | ||||
|                             .font(.custom(Font.bold.rawValue, size: 13.3)) | ||||
|                             .foregroundColor(Color.white) | ||||
|                     } | ||||
|                     .frame(width: 200, height: 33.3, alignment: .center) | ||||
|                     .background(Color(hex: "9970ff")) | ||||
|                     .cornerRadius(4.7) | ||||
|                     .padding(.top, 10.7) | ||||
|                     .onTapGesture { onTapCreateLive() } | ||||
|                 } | ||||
|                 .padding(.vertical, 16.7) | ||||
|                 .frame(width: screenSize().width - 26.7) | ||||
|                 .background(Color(hex: "2b2635")) | ||||
|                 .cornerRadius(4.7) | ||||
|                 .padding(.top, 28.3) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct SectionLiveReservationView_Previews: PreviewProvider { | ||||
|     static var previews: some View { | ||||
|         SectionLiveReservationView( | ||||
|             items: [], | ||||
|             onClickCancel: {}, | ||||
|             onClickStart: { _ in }, | ||||
|             onClickReservation: { _ in }, | ||||
|             onTapCreateLive: {} | ||||
|         ) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										16
									
								
								SodaLive/Sources/Live/Room/GetRoomListRequest.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,16 @@ | ||||
| // | ||||
| //  GetRoomListRequest.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/08/09. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
|  | ||||
| struct GetRoomListRequest { | ||||
|     let timezone: String | ||||
|     let dateString: String? | ||||
|     let status: LiveRoomStatus | ||||
|     let page: Int | ||||
|     let size: Int | ||||
| } | ||||
							
								
								
									
										12
									
								
								SodaLive/Sources/Live/Room/LiveRoomStatus.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,12 @@ | ||||
| // | ||||
| //  LiveRoomStatus.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/08/09. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
|  | ||||
| enum LiveRoomStatus: String { | ||||
|     case NOW, RESERVATION | ||||
| } | ||||
							
								
								
									
										21
									
								
								SodaLive/Sources/Settings/Event/Event.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | ||||
| // | ||||
| //  Event.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/08/09. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
|  | ||||
| struct GetEventResponse: Decodable { | ||||
|     let totalCount: Int | ||||
|     let eventList: [EventItem] | ||||
| } | ||||
|  | ||||
| struct EventItem: Decodable, Hashable { | ||||
|     let id: Int | ||||
|     let thumbnailImageUrl: String | ||||
|     let detailImageUrl: String? | ||||
|     let popupImageUrl: String? | ||||
|     let link: String? | ||||
| } | ||||
							
								
								
									
										48
									
								
								SodaLive/Sources/Settings/Event/EventApi.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,48 @@ | ||||
| // | ||||
| //  EventApi.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/08/09. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
| import Moya | ||||
|  | ||||
| enum EventApi { | ||||
|     case getEvents | ||||
|     case getEventPopup | ||||
| } | ||||
|  | ||||
| extension EventApi: TargetType { | ||||
|     var baseURL: URL { | ||||
|         return URL(string: BASE_URL)! | ||||
|     } | ||||
|      | ||||
|     var path: String { | ||||
|         switch self { | ||||
|         case .getEvents: | ||||
|             return "/event" | ||||
|              | ||||
|         case .getEventPopup: | ||||
|             return "/event/popup" | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var method: Moya.Method { | ||||
|         switch self { | ||||
|         case .getEvents, .getEventPopup: | ||||
|             return .get | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var task: Task { | ||||
|         switch self { | ||||
|         case .getEvents, .getEventPopup: | ||||
|             return .requestPlain | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var headers: [String : String]? { | ||||
|         return ["Authorization": "Bearer \(UserDefaults.string(forKey: UserDefaultsKey.token))"] | ||||
|     } | ||||
| } | ||||
							
								
								
									
										24
									
								
								SodaLive/Sources/Settings/Event/EventRepository.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,24 @@ | ||||
| // | ||||
| //  EventRepository.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/08/09. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
| import CombineMoya | ||||
| import Combine | ||||
| import Moya | ||||
|  | ||||
| final class EventRepository { | ||||
|     private let api = MoyaProvider<EventApi>() | ||||
|      | ||||
|     func getEvents() -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.getEvents) | ||||
|     } | ||||
|      | ||||
|     func getEventPopup() -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.getEventPopup) | ||||
|     } | ||||
| } | ||||
|  | ||||
 Yu Sung
					Yu Sung