새로운 콘텐츠 전체보기 페이지 추가
This commit is contained in:
		| @@ -107,4 +107,6 @@ enum AppStep { | ||||
|     case followingList | ||||
|      | ||||
|     case orderListAll | ||||
|      | ||||
|     case newContentAll | ||||
| } | ||||
|   | ||||
							
								
								
									
										56
									
								
								SodaLive/Sources/Content/All/ContentNewAllItemView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								SodaLive/Sources/Content/All/ContentNewAllItemView.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| // | ||||
| //  ContentNewAllItemView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/09/27. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
| import Kingfisher | ||||
|  | ||||
| struct ContentNewAllItemView: View { | ||||
|      | ||||
|     let item: GetAudioContentMainItem | ||||
|      | ||||
|     @State var width: CGFloat = 0 | ||||
|      | ||||
|     var body: some View { | ||||
|         VStack(alignment: .leading, spacing: 8) { | ||||
|             ZStack(alignment: .topLeading) { | ||||
|                 KFImage(URL(string: item.coverImageUrl)) | ||||
|                     .resizable() | ||||
|                     .scaledToFill() | ||||
|                     .frame(width: width, height: width, alignment: .top) | ||||
|                     .cornerRadius(2.7) | ||||
|             } | ||||
|              | ||||
|             Text(item.title) | ||||
|                 .font(.custom(Font.medium.rawValue, size: 13.3)) | ||||
|                 .foregroundColor(Color(hex: "d2d2d2")) | ||||
|                 .frame(width: width, alignment: .leading) | ||||
|                 .multilineTextAlignment(.leading) | ||||
|                 .fixedSize(horizontal: false, vertical: true) | ||||
|                 .lineLimit(2) | ||||
|              | ||||
|             HStack(spacing: 5.3) { | ||||
|                 KFImage(URL(string: item.creatorProfileImageUrl)) | ||||
|                     .resizable() | ||||
|                     .scaledToFill() | ||||
|                     .frame(width: 21.3, height: 21.3) | ||||
|                     .clipShape(Circle()) | ||||
|                     .onTapGesture { AppState.shared.setAppStep(step: .creatorDetail(userId: item.creatorId)) } | ||||
|                  | ||||
|                 Text(item.creatorNickname) | ||||
|                     .font(.custom(Font.medium.rawValue, size: 12)) | ||||
|                     .foregroundColor(Color(hex: "777777")) | ||||
|                     .lineLimit(1) | ||||
|             } | ||||
|             .padding(.bottom, 10) | ||||
|         } | ||||
|         .frame(width: width, alignment: .leading) | ||||
|         .onTapGesture { AppState.shared.setAppStep(step: .contentDetail(contentId: item.contentId)) } | ||||
|         .onAppear { | ||||
|             width = (screenSize().width - 40) / 2 | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										81
									
								
								SodaLive/Sources/Content/All/ContentNewAllView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								SodaLive/Sources/Content/All/ContentNewAllView.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| // | ||||
| //  ContentNewAllView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/09/27. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct ContentNewAllView: View { | ||||
|      | ||||
|     @StateObject var viewModel = ContentNewAllViewModel() | ||||
|      | ||||
|     let columns = [ | ||||
|         GridItem(.flexible()), | ||||
|         GridItem(.flexible()) | ||||
|     ] | ||||
|      | ||||
|     var body: some View { | ||||
|         BaseView(isLoading: $viewModel.isLoading) { | ||||
|             VStack(spacing: 0) { | ||||
|                 DetailNavigationBar(title: "새로운 콘텐츠") | ||||
|                  | ||||
|                 Text("※ 최근 2주간 등록된 새로운 콘텐츠 입니다.") | ||||
|                     .font(.custom(Font.medium.rawValue, size: 14.7)) | ||||
|                     .foregroundColor(Color(hex: "bbbbbb")) | ||||
|                     .padding(.horizontal, 13.3) | ||||
|                     .padding(.vertical, 8) | ||||
|                     .frame(width: screenSize().width, alignment: .leading) | ||||
|                     .background(Color(hex: "222222")) | ||||
|                     .padding(.top, 13.3) | ||||
|                  | ||||
|                 ContentMainNewContentThemeView( | ||||
|                     themes: viewModel.themeList, | ||||
|                     selectTheme: { | ||||
|                         viewModel.selectedTheme = $0 | ||||
|                     }, | ||||
|                     selectedTheme: $viewModel.selectedTheme | ||||
|                 ).padding(.top, 13.3) | ||||
|                  | ||||
|                 HStack(spacing: 0) { | ||||
|                     Text("전체") | ||||
|                         .font(.custom(Font.medium.rawValue, size: 13.3)) | ||||
|                         .foregroundColor(Color(hex: "e2e2e2")) | ||||
|                      | ||||
|                     Text("\(viewModel.totalCount)") | ||||
|                         .font(.custom(Font.medium.rawValue, size: 13.3)) | ||||
|                         .foregroundColor(Color(hex: "ff5c49")) | ||||
|                         .padding(.leading, 8) | ||||
|                      | ||||
|                     Text("개") | ||||
|                         .font(.custom(Font.medium.rawValue, size: 13.3)) | ||||
|                         .foregroundColor(Color(hex: "e2e2e2")) | ||||
|                         .padding(.leading, 2) | ||||
|                      | ||||
|                     Spacer() | ||||
|                 } | ||||
|                 .padding(.vertical, 13.3) | ||||
|                 .padding(.horizontal, 20) | ||||
|                  | ||||
|                 ScrollView(.vertical, showsIndicators: false) { | ||||
|                     LazyVGrid(columns: columns, spacing: 13.3) { | ||||
|                         ForEach(0..<viewModel.newContentList.count, id: \.self) { | ||||
|                             ContentNewAllItemView(item: viewModel.newContentList[$0]) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             .onAppear { | ||||
|                 viewModel.getThemeList() | ||||
|                 viewModel.getNewContentList() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct ContentNewAllView_Previews: PreviewProvider { | ||||
|     static var previews: some View { | ||||
|         ContentNewAllView() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										130
									
								
								SodaLive/Sources/Content/All/ContentNewAllViewModel.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								SodaLive/Sources/Content/All/ContentNewAllViewModel.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,130 @@ | ||||
| // | ||||
| //  ContentNewAllViewModel.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/09/27. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
| import Combine | ||||
|  | ||||
| final class ContentNewAllViewModel: ObservableObject { | ||||
|      | ||||
|     private let repository = ContentRepository() | ||||
|     private var subscription = Set<AnyCancellable>() | ||||
|      | ||||
|     @Published var errorMessage = "" | ||||
|     @Published var isShowPopup = false | ||||
|     @Published var isLoading = false | ||||
|      | ||||
|     @Published var themeList = [String]() | ||||
|     @Published var newContentList = [GetAudioContentMainItem]() | ||||
|      | ||||
|     @Published var selectedTheme = "전체" { | ||||
|         didSet { | ||||
|             page = 1 | ||||
|             isLast = false | ||||
|             getNewContentList() | ||||
|         } | ||||
|     } | ||||
|     @Published var totalCount = 0 | ||||
|      | ||||
|     var page = 1 | ||||
|     var isLast = false | ||||
|     private let pageSize = 10 | ||||
|      | ||||
|     func getNewContentList() { | ||||
|         if (!isLast && !isLoading) { | ||||
|             isLoading = true | ||||
|              | ||||
|             repository.getNewContentAllOfTheme( | ||||
|                 theme: selectedTheme == "전체" ? "" : selectedTheme, | ||||
|                 page: page, | ||||
|                 size: pageSize | ||||
|             ) | ||||
|                 .sink { result in | ||||
|                     switch result { | ||||
|                     case .finished: | ||||
|                         DEBUG_LOG("finish") | ||||
|                     case .failure(let error): | ||||
|                         ERROR_LOG(error.localizedDescription) | ||||
|                     } | ||||
|                 } receiveValue: { [unowned self] response in | ||||
|                     self.isLoading = false | ||||
|                     let responseData = response.data | ||||
|                      | ||||
|                     do { | ||||
|                         let jsonDecoder = JSONDecoder() | ||||
|                         let decoded = try jsonDecoder.decode(ApiResponse<GetNewContentAllResponse>.self, from: responseData) | ||||
|                         self.isLoading = false | ||||
|                          | ||||
|                         if let data = decoded.data, decoded.success { | ||||
|                             if page == 1 { | ||||
|                                 newContentList.removeAll() | ||||
|                             } | ||||
|                              | ||||
|                             self.totalCount = data.totalCount | ||||
|                              | ||||
|                             if !data.items.isEmpty { | ||||
|                                 page += 1 | ||||
|                                 self.newContentList.append(contentsOf: data.items) | ||||
|                             } else { | ||||
|                                 isLast = true | ||||
|                             } | ||||
|                         } else { | ||||
|                             if let message = decoded.message { | ||||
|                                 self.errorMessage = message | ||||
|                             } else { | ||||
|                                 self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." | ||||
|                             } | ||||
|                              | ||||
|                             self.isShowPopup = true | ||||
|                         } | ||||
|                     } catch { | ||||
|                         self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." | ||||
|                         self.isShowPopup = true | ||||
|                         self.isLoading = false | ||||
|                     } | ||||
|                 } | ||||
|                 .store(in: &subscription) | ||||
|         } else { | ||||
|             isLoading = false | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     func getThemeList() { | ||||
|         repository.getNewContentThemeList() | ||||
|             .sink { result in | ||||
|                 switch result { | ||||
|                 case .finished: | ||||
|                     DEBUG_LOG("finish") | ||||
|                 case .failure(let error): | ||||
|                     ERROR_LOG(error.localizedDescription) | ||||
|                 } | ||||
|             } receiveValue: { [unowned self] response in | ||||
|                 let responseData = response.data | ||||
|                  | ||||
|                 do { | ||||
|                     let jsonDecoder = JSONDecoder() | ||||
|                     let decoded = try jsonDecoder.decode(ApiResponse<[String]>.self, from: responseData) | ||||
|                      | ||||
|                     if let data = decoded.data, decoded.success { | ||||
|                         self.themeList.append("전체") | ||||
|                         self.themeList.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) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										11
									
								
								SodaLive/Sources/Content/All/GetNewContentAllResponse.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								SodaLive/Sources/Content/All/GetNewContentAllResponse.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| // | ||||
| //  GetNewContentAllResponse.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2023/09/27. | ||||
| // | ||||
|  | ||||
| struct GetNewContentAllResponse: Decodable { | ||||
|     let totalCount: Int | ||||
|     let items: [GetAudioContentMainItem] | ||||
| } | ||||
| @@ -26,6 +26,8 @@ enum ContentApi { | ||||
|     case getNewContentOfTheme(theme: String) | ||||
|     case donation(request: AudioContentDonationRequest) | ||||
|     case modifyComment(request: ModifyCommentRequest) | ||||
|     case getNewContentThemeList | ||||
|     case getNewContentAllOfTheme(theme: String, page: Int, size: Int) | ||||
| } | ||||
|  | ||||
| extension ContentApi: TargetType { | ||||
| @@ -85,12 +87,18 @@ extension ContentApi: TargetType { | ||||
|              | ||||
|         case .modifyComment: | ||||
|             return "/audio-content/comment" | ||||
|              | ||||
|         case .getNewContentThemeList: | ||||
|             return "/audio-content/main/theme" | ||||
|              | ||||
|         case .getNewContentAllOfTheme: | ||||
|             return "/audio-content/main/new/all" | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var method: Moya.Method { | ||||
|         switch self { | ||||
|         case .getAudioContentList, .getAudioContentDetail, .getOrderList, .getAudioContentThemeList, .getAudioContentCommentList, .getAudioContentCommentReplyList, .getMain, .getNewContentOfTheme: | ||||
|         case .getAudioContentList, .getAudioContentDetail, .getOrderList, .getAudioContentThemeList, .getAudioContentCommentList, .getAudioContentCommentReplyList, .getMain, .getNewContentOfTheme, .getNewContentThemeList, .getNewContentAllOfTheme: | ||||
|             return .get | ||||
|              | ||||
|         case .likeContent, .modifyAudioContent, .modifyComment: | ||||
| @@ -180,6 +188,18 @@ extension ContentApi: TargetType { | ||||
|              | ||||
|         case .modifyComment(let request): | ||||
|             return .requestJSONEncodable(request) | ||||
|              | ||||
|         case .getNewContentThemeList: | ||||
|             return .requestPlain | ||||
|              | ||||
|         case .getNewContentAllOfTheme(let theme, let page, let size): | ||||
|             let parameters = [ | ||||
|                 "theme": theme, | ||||
|                 "page": page - 1, | ||||
|                 "size": size | ||||
|             ] as [String : Any] | ||||
|              | ||||
|             return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|   | ||||
| @@ -80,4 +80,12 @@ final class ContentRepository { | ||||
|     func modifyComment(request: ModifyCommentRequest) -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.modifyComment(request: request)) | ||||
|     } | ||||
|      | ||||
|     func getNewContentThemeList() -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.getNewContentThemeList) | ||||
|     } | ||||
|      | ||||
|     func getNewContentAllOfTheme(theme: String, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.getNewContentAllOfTheme(theme: theme, page: page, size: size)) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -13,9 +13,20 @@ struct ContentMainCurationItemView: View { | ||||
|      | ||||
|     var body: some View { | ||||
|         VStack(alignment: .leading, spacing: 0) { | ||||
|             Text(item.title) | ||||
|                 .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                 .foregroundColor(Color(hex: "eeeeee")) | ||||
|             HStack(spacing: 0) { | ||||
|                 Text(item.title) | ||||
|                     .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                     .foregroundColor(Color(hex: "eeeeee")) | ||||
|                  | ||||
|                 Spacer() | ||||
|                  | ||||
|                 Image("ic_forward") | ||||
|                     .resizable() | ||||
|                     .frame(width: 20, height: 20) | ||||
|                     .onTapGesture { | ||||
|                          | ||||
|                     } | ||||
|             } | ||||
|              | ||||
|             Text(item.description) | ||||
|                 .font(.custom(Font.medium.rawValue, size: 13)) | ||||
|   | ||||
| @@ -6,7 +6,6 @@ | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
| import GoogleMobileAds | ||||
|  | ||||
| struct ContentMainCurationView: View { | ||||
|      | ||||
| @@ -18,14 +17,6 @@ struct ContentMainCurationView: View { | ||||
|                 let item = items[$0] | ||||
|                 ContentMainCurationItemView(item: item) | ||||
|                     .padding(.horizontal, 13.3) | ||||
|                  | ||||
|                 if $0 % 3 == 2 { | ||||
|                     BannerAdView(adUnitId: CURATION_BANNER_AD_UNIT_ID) | ||||
|                         .frame( | ||||
|                             width: screenSize().width, | ||||
|                             height: GADCurrentOrientationAnchoredAdaptiveBannerAdSizeWithWidth(screenSize().width).size.height | ||||
|                         ) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -33,7 +33,11 @@ struct ContentMainNewContentThemeView: View { | ||||
|                                 .stroke(lineWidth: 0.5) | ||||
|                                 .foregroundColor(Color(hex: selectedTheme == theme ? "9970ff" : "eeeeee")) | ||||
|                         ) | ||||
|                         .onTapGesture { selectTheme(theme) } | ||||
|                         .onTapGesture { | ||||
|                             if selectedTheme != theme { | ||||
|                                 selectTheme(theme) | ||||
|                             } | ||||
|                         } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -17,9 +17,20 @@ struct ContentMainNewContentView: View { | ||||
|      | ||||
|     var body: some View { | ||||
|         VStack(alignment: .leading, spacing: 0) { | ||||
|             Text("새로운 콘텐츠") | ||||
|                 .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                 .foregroundColor(Color(hex: "eeeeee")) | ||||
|             HStack(spacing: 0) { | ||||
|                 Text("새로운 콘텐츠") | ||||
|                     .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                     .foregroundColor(Color(hex: "eeeeee")) | ||||
|                  | ||||
|                 Spacer() | ||||
|                  | ||||
|                 Image("ic_forward") | ||||
|                     .resizable() | ||||
|                     .frame(width: 20, height: 20) | ||||
|                     .onTapGesture { | ||||
|                         AppState.shared.setAppStep(step: .newContentAll) | ||||
|                     } | ||||
|             } | ||||
|              | ||||
|             ContentMainNewContentThemeView(themes: themes, selectTheme: selectTheme, selectedTheme: $selectedTheme) | ||||
|                 .padding(.vertical, 16.7) | ||||
|   | ||||
| @@ -157,6 +157,9 @@ struct ContentView: View { | ||||
|             case .userProfileFanTalkAll(let userId): | ||||
|                 UserProfileFanTalkAllView(userId: userId) | ||||
|                  | ||||
|             case .newContentAll: | ||||
|                 ContentNewAllView() | ||||
|                  | ||||
|             default: | ||||
|                 EmptyView() | ||||
|                     .frame(width: 0, height: 0, alignment: .topLeading) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Yu Sung
					Yu Sung