콘텐츠 메인
- 단편 탭 UI 페이지 생성
This commit is contained in:
		| @@ -45,6 +45,11 @@ enum ContentApi { | ||||
|     case getContentMainSeries | ||||
|     case getRecommendSeriesListByGenre(genreId: Int) | ||||
|     case getRecommendSeriesByCreator(creatorId: Int) | ||||
|      | ||||
|     case getContentMainContent | ||||
|     case getContentMainNewContentOfTheme(theme: String, isAdultContentVisible: Bool, contentType: ContentType) | ||||
|     case getDailyContentRanking(sortType: String) | ||||
|     case getRecommendContentByTag(tag: String) | ||||
| } | ||||
|  | ||||
| extension ContentApi: TargetType { | ||||
| @@ -155,6 +160,18 @@ extension ContentApi: TargetType { | ||||
|              | ||||
|         case .getRecommendSeriesByCreator: | ||||
|             return "/v2/audio-content/main/series/recommend-series-by-creator" | ||||
|              | ||||
|         case .getContentMainContent: | ||||
|             return "/v2/audio-content/main/content" | ||||
|              | ||||
|         case .getContentMainNewContentOfTheme: | ||||
|             return "/v2/audio-content/main/content/new-content-by-theme" | ||||
|              | ||||
|         case .getDailyContentRanking: | ||||
|             return "/v2/audio-content/main/content/ranking" | ||||
|              | ||||
|         case .getRecommendContentByTag: | ||||
|             return "/v2/audio-content/main/content/recommend-content-by-tag" | ||||
|         } | ||||
|     } | ||||
|      | ||||
| @@ -169,7 +186,8 @@ extension ContentApi: TargetType { | ||||
|         case .getMainBannerList, .getMainOrderList, .getNewContentUploadCreatorList, .getCurationList, .getAudioContentByTheme: | ||||
|             return .get | ||||
|              | ||||
|         case .getContentMainHome, .getPopularContentByCreator, .getContentMainSeries, .getRecommendSeriesListByGenre, .getRecommendSeriesByCreator: | ||||
|         case .getContentMainHome, .getPopularContentByCreator, .getContentMainSeries, .getRecommendSeriesListByGenre, .getRecommendSeriesByCreator, .getContentMainContent, | ||||
|                 .getContentMainNewContentOfTheme, .getDailyContentRanking, .getRecommendContentByTag: | ||||
|             return .get | ||||
|              | ||||
|         case .likeContent, .modifyAudioContent, .modifyComment, .unpinContent: | ||||
| @@ -326,7 +344,7 @@ extension ContentApi: TargetType { | ||||
|              | ||||
|             return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) | ||||
|              | ||||
|         case .getContentMainHome, .getContentMainSeries: | ||||
|         case .getContentMainHome, .getContentMainSeries, .getContentMainContent: | ||||
|             return .requestPlain | ||||
|              | ||||
|         case .getRecommendSeriesListByGenre(let genreId): | ||||
| @@ -340,6 +358,22 @@ extension ContentApi: TargetType { | ||||
|         case .getRecommendSeriesByCreator(let creatorId): | ||||
|             let parameters = ["creatorId": creatorId] | ||||
|             return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) | ||||
|              | ||||
|         case .getContentMainNewContentOfTheme(let theme, let isAdultContentVisible, let contentType): | ||||
|             let parameters = [ | ||||
|                 "theme": theme, | ||||
|                 "isAdultContentVisible": isAdultContentVisible, | ||||
|                 "contentType": contentType | ||||
|             ] as [String : Any] | ||||
|             return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) | ||||
|              | ||||
|         case .getDailyContentRanking(let sortType): | ||||
|             let parameters = ["sortType": sortType] | ||||
|             return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) | ||||
|              | ||||
|         case .getRecommendContentByTag(let tag): | ||||
|             let parameters = ["tag": tag] | ||||
|             return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|   | ||||
| @@ -40,6 +40,7 @@ struct ContentMainRankingSortView: View { | ||||
|                         } | ||||
|                 } | ||||
|             } | ||||
|             .padding(.horizontal, 13.3) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,42 @@ | ||||
| // | ||||
| //  ContentMainTabContentRepository.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2/21/25. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
| import CombineMoya | ||||
| import Combine | ||||
| import Moya | ||||
|  | ||||
| final class ContentMainTabContentRepository { | ||||
|      | ||||
|     private let api = MoyaProvider<ContentApi>() | ||||
|      | ||||
|     func getContentMainContent() -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.getContentMainContent) | ||||
|     } | ||||
|      | ||||
|     func getNewContentOfTheme(theme: String) -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher( | ||||
|             .getContentMainNewContentOfTheme( | ||||
|                 theme: theme, | ||||
|                 isAdultContentVisible: UserDefaults.isAdultContentVisible(), | ||||
|                 contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL | ||||
|             ) | ||||
|         ) | ||||
|     } | ||||
|      | ||||
|     func getContentRanking(sortType: String) -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.getDailyContentRanking(sortType: sortType)) | ||||
|     } | ||||
|      | ||||
|     func getRecommendContentByTag(tag: String) -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.getRecommendContentByTag(tag: tag)) | ||||
|     } | ||||
|      | ||||
|     func getPopularContentByCreator(creatorId: Int) -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.getPopularContentByCreator(creatorId: creatorId)) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,104 @@ | ||||
| // | ||||
| //  ContentMainTabContentView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2/21/25. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct ContentMainTabContentView: View { | ||||
|      | ||||
|     @StateObject var viewModel = ContentMainTabContentViewModel() | ||||
|      | ||||
|     var body: some View { | ||||
|         BaseView(isLoading: $viewModel.isLoading) { | ||||
|             ScrollView(.vertical, showsIndicators: false) { | ||||
|                 VStack(spacing: 0) { | ||||
|                     if !viewModel.bannerList.isEmpty { | ||||
|                         ContentMainBannerViewV2(bannerList: viewModel.bannerList) | ||||
|                             .padding(.horizontal, 13.3) | ||||
|                     } | ||||
|                      | ||||
|                     if !viewModel.contentThemeList.isEmpty { | ||||
|                         ContentMainNewContentViewV2( | ||||
|                             title: "새로운 단편", | ||||
|                             onClickMore: {}, | ||||
|                             themeList: viewModel.contentThemeList, | ||||
|                             contentList: viewModel.newContentList | ||||
|                         ) { | ||||
|                             viewModel.getNewContentOfTheme(theme: $0) | ||||
|                         } | ||||
|                         .padding(.top, 30) | ||||
|                     } | ||||
|                      | ||||
|                     if !viewModel.rankSortTypeList.isEmpty { | ||||
|                         ContentMainTabRankContentView( | ||||
|                             title: "일간 랭킹", | ||||
|                             isMore: false, | ||||
|                             onClickMore: {}, | ||||
|                             sortList: viewModel.rankSortTypeList, | ||||
|                             onClickSort: { viewModel.getContentRanking(sort: $0) }, | ||||
|                             contentList: viewModel.rankContentList | ||||
|                         ) | ||||
|                         .padding(.top, 30) | ||||
|                     } | ||||
|                      | ||||
|                     if !viewModel.contentRankCreatorList.isEmpty { | ||||
|                         ContentByChannelView( | ||||
|                             title: "채널별 추천 단편", | ||||
|                             creatorList: viewModel.contentRankCreatorList, | ||||
|                             contentList: viewModel.salesCountRankContentList, | ||||
|                             onClickCreator: { | ||||
|                                 viewModel.getPopularContentByCreator(creatorId: $0) | ||||
|                             } | ||||
|                         ) | ||||
|                         .padding(.top, 30) | ||||
|                     } | ||||
|                      | ||||
|                     if !viewModel.eventBannerList.isEmpty { | ||||
|                         SectionEventBannerView(items: viewModel.eventBannerList) | ||||
|                             .padding(.top, 30) | ||||
|                     } | ||||
|                      | ||||
|                     if !viewModel.tagList.isEmpty { | ||||
|                         ContentMainTagCurationView( | ||||
|                             tagList: viewModel.tagList, | ||||
|                             contentList: viewModel.tagCurationContentList | ||||
|                         ) { | ||||
|                             viewModel.getRecommendContentByTag(tag: $0) | ||||
|                         } | ||||
|                         .padding(.top, 30) | ||||
|                     } | ||||
|                      | ||||
|                     if !viewModel.curationList.isEmpty { | ||||
|                         ContentMainCurationViewV2(curationList: viewModel.curationList) | ||||
|                         .padding(.top, 30) | ||||
|                     } | ||||
|                 } | ||||
|                 .onAppear { | ||||
|                     viewModel.fetchData() | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { | ||||
|             HStack { | ||||
|                 Spacer() | ||||
|                 Text(viewModel.errorMessage) | ||||
|                     .padding(.vertical, 13.3) | ||||
|                     .frame(width: screenSize().width - 66.7, alignment: .center) | ||||
|                     .font(.custom(Font.medium.rawValue, size: 12)) | ||||
|                     .background(Color.button) | ||||
|                     .foregroundColor(Color.white) | ||||
|                     .multilineTextAlignment(.leading) | ||||
|                     .cornerRadius(20) | ||||
|                     .padding(.bottom, 66.7) | ||||
|                 Spacer() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     ContentMainTabContentView() | ||||
| } | ||||
| @@ -0,0 +1,231 @@ | ||||
| // | ||||
| //  ContentMainTabContentViewModel.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2/21/25. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
| import Combine | ||||
|  | ||||
| final class ContentMainTabContentViewModel: ObservableObject { | ||||
|     private let repository = ContentMainTabContentRepository() | ||||
|     private var subscription = Set<AnyCancellable>() | ||||
|      | ||||
|     @Published var errorMessage = "" | ||||
|     @Published var isShowPopup = false | ||||
|     @Published var isLoading = false | ||||
|      | ||||
|     @Published var bannerList: [GetAudioContentBannerResponse] = [] | ||||
|     @Published var contentThemeList: [String] = [] | ||||
|     @Published var newContentList: [GetAudioContentMainItem] = [] | ||||
|     @Published var rankSortTypeList: [String] = [] | ||||
|     @Published var rankContentList: [GetAudioContentRankingItem] = [] | ||||
|     @Published var contentRankCreatorList: [ContentCreatorResponse] = [] | ||||
|     @Published var salesCountRankContentList: [GetAudioContentRankingItem] = [] | ||||
|     @Published var eventBannerList: [EventItem] = [] | ||||
|     @Published var tagList: [String] = [] | ||||
|     @Published var tagCurationContentList: [GetAudioContentMainItem] = [] | ||||
|     @Published var curationList: [GetContentCurationResponse] = [] | ||||
|      | ||||
|     func fetchData() { | ||||
|         isLoading = true | ||||
|         repository.getContentMainContent() | ||||
|             .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<GetContentMainTabContentResponse>.self, from: responseData) | ||||
|                      | ||||
|                     if let data = decoded.data, decoded.success { | ||||
|                         self.bannerList = data.bannerList | ||||
|                         self.contentThemeList = ["전체"] + data.contentThemeList | ||||
|                         self.newContentList = data.newContentList | ||||
|                         self.rankSortTypeList = data.rankSortTypeList | ||||
|                         self.rankContentList = data.rankContentList | ||||
|                         self.contentRankCreatorList = data.contentRankCreatorList | ||||
|                         self.salesCountRankContentList = data.salesCountRankContentList | ||||
|                         self.eventBannerList = data.eventBannerList.eventList | ||||
|                         self.tagList = data.tagList | ||||
|                         self.tagCurationContentList = data.tagCurationContentList | ||||
|                         self.curationList = data.curationList | ||||
|                     } 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) | ||||
|     } | ||||
|      | ||||
|     func getNewContentOfTheme(theme: String) { | ||||
|         isLoading = true | ||||
|          | ||||
|         repository.getNewContentOfTheme(theme: theme == "전체" ? "" : theme) | ||||
|             .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<[GetAudioContentMainItem]>.self, from: responseData) | ||||
|                      | ||||
|                     if let data = decoded.data, decoded.success { | ||||
|                         self.newContentList = data | ||||
|                     } 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) | ||||
|     } | ||||
|      | ||||
|     func getContentRanking(sort: String = "매출") { | ||||
|         isLoading = true | ||||
|         repository.getContentRanking(sortType: sort) | ||||
|             .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<[GetAudioContentRankingItem]>.self, from: responseData) | ||||
|                      | ||||
|                     if let data = decoded.data, decoded.success { | ||||
|                         self.rankContentList = data | ||||
|                     } 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) | ||||
|     } | ||||
|      | ||||
|     func getRecommendContentByTag(tag: String) { | ||||
|         isLoading = true | ||||
|         repository.getRecommendContentByTag(tag: tag) | ||||
|             .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<[GetAudioContentMainItem]>.self, from: responseData) | ||||
|                      | ||||
|                     if let data = decoded.data, decoded.success { | ||||
|                         self.tagCurationContentList = data | ||||
|                     } 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) | ||||
|     } | ||||
|      | ||||
|     func getPopularContentByCreator(creatorId: Int) { | ||||
|         isLoading = true | ||||
|         repository.getPopularContentByCreator(creatorId: creatorId) | ||||
|             .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<[GetAudioContentRankingItem]>.self, from: responseData) | ||||
|                      | ||||
|                     if let data = decoded.data, decoded.success { | ||||
|                         self.salesCountRankContentList = data | ||||
|                     } 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) | ||||
|     } | ||||
| } | ||||
| @@ -43,6 +43,7 @@ struct ContentMainTabRankContentView: View { | ||||
|                         .onTapGesture { onClickMore() } | ||||
|                 } | ||||
|             } | ||||
|             .padding(.horizontal, 13.3) | ||||
|              | ||||
|             if !sortList.isEmpty { | ||||
|                 ContentMainRankingSortView( | ||||
| @@ -97,6 +98,7 @@ struct ContentMainTabRankContentView: View { | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 .padding(.horizontal, 13.3) | ||||
|                 .frame(height: 207) | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -0,0 +1,231 @@ | ||||
| // | ||||
| //  ContentMainTagCurationView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2/22/25. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
| import Kingfisher | ||||
|  | ||||
| struct ContentMainTagCurationView: View { | ||||
|      | ||||
|     let tagList: [String] | ||||
|     let contentList: [GetAudioContentMainItem] | ||||
|     let selectTag: (String) -> Void | ||||
|      | ||||
|     let tagColumns = [ | ||||
|         GridItem(.flexible()), | ||||
|         GridItem(.flexible()), | ||||
|         GridItem(.flexible()), | ||||
|         GridItem(.flexible()) | ||||
|     ] | ||||
|      | ||||
|     let contentColumns = [ | ||||
|         GridItem(.flexible()), | ||||
|         GridItem(.flexible()), | ||||
|         GridItem(.flexible()) | ||||
|     ] | ||||
|      | ||||
|     @State private var selectedTag = "" | ||||
|      | ||||
|     var body: some View { | ||||
|         VStack(alignment: .leading, spacing: 13.3) { | ||||
|             Text("태그별 추천 콘텐츠") | ||||
|                 .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                 .foregroundColor(.grayee) | ||||
|                 .padding(.horizontal, 13.3) | ||||
|              | ||||
|             LazyVGrid(columns: tagColumns, spacing: 6) { | ||||
|                 ForEach(0..<tagList.count, id: \.self) { index in | ||||
|                     let tag = tagList[index] | ||||
|                     Text(tagList[index]) | ||||
|                         .font(.custom(Font.medium.rawValue, size: 10)) | ||||
|                         .foregroundColor( | ||||
|                             selectedTag == tag ? | ||||
|                                 .button: | ||||
|                                 .gray77 | ||||
|                         ) | ||||
|                         .padding(.vertical, 10) | ||||
|                         .frame(width: (screenSize().width - 18 - 26.7) / 4) | ||||
|                         .overlay( | ||||
|                             RoundedRectangle(cornerRadius: 2.6) | ||||
|                                 .strokeBorder(lineWidth: 1) | ||||
|                                 .foregroundColor( | ||||
|                                     selectedTag == tag ? | ||||
|                                         .button: | ||||
|                                         .gray77 | ||||
|                                 ) | ||||
|                         ) | ||||
|                         .onTapGesture { | ||||
|                             if selectedTag != tag { | ||||
|                                 selectedTag = tag | ||||
|                                 selectTag(tag) | ||||
|                             } | ||||
|                         } | ||||
|                 } | ||||
|             } | ||||
|             .padding(.horizontal, 13.3) | ||||
|              | ||||
|             LazyVGrid(columns: contentColumns, spacing: 13.3) { | ||||
|                 ForEach(0..<contentList.count, id: \.self) { index in | ||||
|                     ContentMainTagCurationContentView( | ||||
|                         item: contentList[index], | ||||
|                         itemWidth: (screenSize().width - 40) / 3 | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|             .padding(.horizontal, 13.3) | ||||
|         } | ||||
|         .onAppear { | ||||
|             selectedTag = tagList[0] | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct ContentMainTagCurationContentView: View { | ||||
|      | ||||
|     let item: GetAudioContentMainItem | ||||
|     let itemWidth: CGFloat | ||||
|      | ||||
|     var body: some View { | ||||
|         VStack(alignment: .leading, spacing: 8) { | ||||
|             ZStack(alignment: .bottom) { | ||||
|                 KFImage(URL(string: item.coverImageUrl)) | ||||
|                     .cancelOnDisappear(true) | ||||
|                     .downsampling( | ||||
|                         size: CGSize( | ||||
|                             width: itemWidth, | ||||
|                             height: itemWidth | ||||
|                         ) | ||||
|                     ) | ||||
|                     .resizable() | ||||
|                     .scaledToFill() | ||||
|                     .frame(width: itemWidth, height: itemWidth, alignment: .top) | ||||
|                     .cornerRadius(2.7) | ||||
|                  | ||||
|                 VStack(spacing: 0) { | ||||
|                     Spacer() | ||||
|                      | ||||
|                     HStack(spacing: 0) { | ||||
|                         HStack(spacing: 2) { | ||||
|                             if item.price > 0 { | ||||
|                                 Image("ic_card_can_gray") | ||||
|                                  | ||||
|                                 Text("\(item.price)") | ||||
|                                     .font(.custom(Font.medium.rawValue, size: 8.5)) | ||||
|                                     .foregroundColor(Color.white) | ||||
|                             } else { | ||||
|                                 Text("무료") | ||||
|                                     .font(.custom(Font.medium.rawValue, size: 8.5)) | ||||
|                                     .foregroundColor(Color.white) | ||||
|                             } | ||||
|                         } | ||||
|                         .padding(3) | ||||
|                         .background(Color.gray33.opacity(0.7)) | ||||
|                         .cornerRadius(10) | ||||
|                         .padding(.leading, 2.7) | ||||
|                         .padding(.bottom, 2.7) | ||||
|                          | ||||
|                         Spacer() | ||||
|                          | ||||
|                         HStack(spacing: 2) { | ||||
|                             Text(item.duration) | ||||
|                                 .font(.custom(Font.medium.rawValue, size: 8.5)) | ||||
|                                 .foregroundColor(Color.white) | ||||
|                         } | ||||
|                         .padding(3) | ||||
|                         .background(Color.gray33.opacity(0.7)) | ||||
|                         .cornerRadius(10) | ||||
|                         .padding(.trailing, 2.7) | ||||
|                         .padding(.bottom, 2.7) | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             .frame(width: itemWidth, height: itemWidth) | ||||
|              | ||||
|             Text(item.title) | ||||
|                 .font(.custom(Font.medium.rawValue, size: 13.3)) | ||||
|                 .foregroundColor(Color.grayd2) | ||||
|                 .frame(width: itemWidth, alignment: .leading) | ||||
|                 .multilineTextAlignment(.leading) | ||||
|                 .fixedSize(horizontal: false, vertical: true) | ||||
|                 .lineLimit(2) | ||||
|              | ||||
|             HStack(spacing: 5.3) { | ||||
|                 KFImage(URL(string: item.creatorProfileImageUrl)) | ||||
|                     .cancelOnDisappear(true) | ||||
|                     .downsampling( | ||||
|                         size: CGSize( | ||||
|                             width: 21.3, | ||||
|                             height: 21.3 | ||||
|                         ) | ||||
|                     ) | ||||
|                     .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(.gray77) | ||||
|                     .lineLimit(1) | ||||
|             } | ||||
|             .padding(.bottom, 10) | ||||
|         } | ||||
|         .onTapGesture { | ||||
|             AppState.shared | ||||
|                 .setAppStep(step: .contentDetail(contentId: item.contentId)) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     ContentMainTagCurationView( | ||||
|         tagList: ["test", "test2", "test3", "test4", "test5", "test6", "test7"], | ||||
|         contentList: [ | ||||
|             GetAudioContentMainItem( | ||||
|                 contentId: 1, | ||||
|                 coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 title: "ㅓ처랴햐햫햐햐", | ||||
|                 creatorId: 8, | ||||
|                 creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 creatorNickname: "유저1", | ||||
|                 price: 100, | ||||
|                 duration: "00:00:30" | ||||
|             ), | ||||
|             GetAudioContentMainItem( | ||||
|                 contentId: 2, | ||||
|                 coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 title: "ㅓ처랴햐햫햐햐", | ||||
|                 creatorId: 8, | ||||
|                 creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 creatorNickname: "유저2", | ||||
|                 price: 0, | ||||
|                 duration: "00:00:30" | ||||
|             ), | ||||
|             GetAudioContentMainItem( | ||||
|                 contentId: 3, | ||||
|                 coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 title: "ㅓ처랴햐햫햐햐", | ||||
|                 creatorId: 8, | ||||
|                 creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 creatorNickname: "유저3", | ||||
|                 price: 1000, | ||||
|                 duration: "00:00:30" | ||||
|             ), | ||||
|             GetAudioContentMainItem( | ||||
|                 contentId: 4, | ||||
|                 coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 title: "ㅓ처랴햐햫햐햐", | ||||
|                 creatorId: 8, | ||||
|                 creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 creatorNickname: "유저3", | ||||
|                 price: 50000, | ||||
|                 duration: "00:00:30" | ||||
|             ) | ||||
|         ], | ||||
|         selectTag: { _ in } | ||||
|     ) | ||||
| } | ||||
| @@ -0,0 +1,20 @@ | ||||
| // | ||||
| //  GetContentMainTabContentResponse.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2/21/25. | ||||
| // | ||||
|  | ||||
| struct GetContentMainTabContentResponse: Decodable { | ||||
|     let bannerList: [GetAudioContentBannerResponse] | ||||
|     let contentThemeList: [String] | ||||
|     let newContentList: [GetAudioContentMainItem] | ||||
|     let rankSortTypeList: [String] | ||||
|     let rankContentList: [GetAudioContentRankingItem] | ||||
|     let contentRankCreatorList: [ContentCreatorResponse] | ||||
|     let salesCountRankContentList: [GetAudioContentRankingItem] | ||||
|     let eventBannerList: GetEventResponse | ||||
|     let tagList: [String] | ||||
|     let tagCurationContentList: [GetAudioContentMainItem] | ||||
|     let curationList: [GetContentCurationResponse] | ||||
| } | ||||
| @@ -23,7 +23,8 @@ struct ContentByChannelView: View { | ||||
|         VStack(alignment: .leading, spacing: 20) { | ||||
|             Text(title) | ||||
|                 .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                 .foregroundColor(Color(hex: "eeeeee")) | ||||
|                 .foregroundColor(.grayee) | ||||
|                 .padding(.horizontal, 13.3) | ||||
|              | ||||
|             ScrollView(.horizontal) { | ||||
|                 HStack(spacing: 22) { | ||||
| @@ -41,6 +42,7 @@ struct ContentByChannelView: View { | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 .padding(.horizontal, 13.3) | ||||
|             } | ||||
|              | ||||
|             LazyVGrid(columns: columns, spacing: 13.3) { | ||||
| @@ -111,6 +113,7 @@ struct ContentByChannelView: View { | ||||
|                                 .setAppStep(step: .creatorDetail(userId: content.creatorId)) | ||||
|                         } | ||||
|                     } | ||||
|                     .padding(.horizontal, 13.3) | ||||
|                     .onTapGesture { | ||||
|                         AppState | ||||
|                             .shared | ||||
|   | ||||
| @@ -0,0 +1,56 @@ | ||||
| // | ||||
| //  ContentMainContentThemeView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2/21/25. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct ContentMainContentThemeView: View { | ||||
|      | ||||
|     let themeList: [String] | ||||
|     let selectTheme: (String) -> Void | ||||
|      | ||||
|     @Binding var selectedTheme: String | ||||
|      | ||||
|     var body: some View { | ||||
|         ScrollView(.horizontal, showsIndicators: false) { | ||||
|             HStack(alignment: .top, spacing: 8) { | ||||
|                 ForEach(0..<themeList.count, id: \.self) { index in | ||||
|                     let theme = themeList[index] | ||||
|                     Text(theme) | ||||
|                         .font(.custom(Font.medium.rawValue, size: 14.7)) | ||||
|                         .foregroundColor(selectedTheme == theme ? Color.button : Color.gray77) | ||||
|                         .padding(.horizontal, 13.3) | ||||
|                         .padding(.vertical, 9.3) | ||||
|                         .border( | ||||
|                             selectedTheme == theme ? Color.button : Color.grayee, | ||||
|                             width: 1 | ||||
|                         ) | ||||
|                         .cornerRadius(16.7) | ||||
|                         .overlay( | ||||
|                             RoundedRectangle(cornerRadius: CGFloat(16.7)) | ||||
|                                 .stroke(lineWidth: 1) | ||||
|                                 .foregroundColor(selectedTheme == theme ? Color.button : Color.grayee) | ||||
|                         ) | ||||
|                         .onTapGesture { | ||||
|                             if selectedTheme != theme { | ||||
|                                 selectedTheme = theme | ||||
|                                 selectTheme(theme) | ||||
|                             } | ||||
|                         } | ||||
|                 } | ||||
|             } | ||||
|             .padding(.horizontal, 13.3) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     ContentMainContentThemeView( | ||||
|         themeList: ["전체", "test", "test2"], | ||||
|         selectTheme: { _ in }, | ||||
|         selectedTheme: .constant("전체") | ||||
|     ) | ||||
| } | ||||
| @@ -0,0 +1,87 @@ | ||||
| // | ||||
| //  ContentMainCurationViewV2.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2/21/25. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct ContentMainCurationViewV2: View { | ||||
|      | ||||
|     let curationList: [GetContentCurationResponse] | ||||
|      | ||||
|     var body: some View { | ||||
|         LazyVStack(spacing: 30) { | ||||
|             ForEach(0..<curationList.count, id: \.self) { index in | ||||
|                 let curation = curationList[index] | ||||
|                 ContentMainCurationItemViewV2(curation: curation) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct ContentMainCurationItemViewV2: View { | ||||
|     let curation: GetContentCurationResponse | ||||
|      | ||||
|     var body: some View { | ||||
|         VStack(alignment: .leading, spacing: 13.3) { | ||||
|             Text(curation.title) | ||||
|                 .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                 .foregroundColor(.grayee) | ||||
|                 .padding(.horizontal, 13.3) | ||||
|              | ||||
|             ScrollView(.horizontal, showsIndicators: false) { | ||||
|                 HStack(spacing: 13.3) { | ||||
|                     ForEach(0..<curation.items.count, id: \.self) { index in | ||||
|                         let item = curation.items[index] | ||||
|                         ContentMainItemView(item: item) | ||||
|                     } | ||||
|                 } | ||||
|                 .padding(.horizontal, 13.3) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     ContentMainCurationViewV2( | ||||
|         curationList: [ | ||||
|             GetContentCurationResponse( | ||||
|                 title: "test1", | ||||
|                 items: [ | ||||
|                     GetAudioContentMainItem( | ||||
|                         contentId: 1, | ||||
|                         coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                         title: "ㅓ처랴햐햫햐햐", | ||||
|                         creatorId: 8, | ||||
|                         creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                         creatorNickname: "유저1", | ||||
|                         price: 10, | ||||
|                         duration: "00:00:30" | ||||
|                     ), | ||||
|                     GetAudioContentMainItem( | ||||
|                         contentId: 2, | ||||
|                         coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                         title: "ㅓ처랴햐햫햐햐", | ||||
|                         creatorId: 8, | ||||
|                         creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                         creatorNickname: "유저2", | ||||
|                         price: 10, | ||||
|                         duration: "00:00:30" | ||||
|                     ), | ||||
|                     GetAudioContentMainItem( | ||||
|                         contentId: 3, | ||||
|                         coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                         title: "ㅓ처랴햐햫햐햐", | ||||
|                         creatorId: 8, | ||||
|                         creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                         creatorNickname: "유저3", | ||||
|                         price: 10, | ||||
|                         duration: "00:00:30" | ||||
|                     ) | ||||
|                 ] | ||||
|             ) | ||||
|         ] | ||||
|     ) | ||||
| } | ||||
| @@ -0,0 +1,97 @@ | ||||
| // | ||||
| //  ContentMainNewContentViewV2.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2/21/25. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct ContentMainNewContentViewV2: View { | ||||
|      | ||||
|     let title: String | ||||
|     let onClickMore: () -> Void | ||||
|     let themeList: [String] | ||||
|     let contentList: [GetAudioContentMainItem] | ||||
|      | ||||
|     let selectTheme: (String) -> Void | ||||
|     @State private var selectedTheme = "전체" | ||||
|      | ||||
|     var body: some View { | ||||
|         LazyVStack(spacing: 13.3) { | ||||
|             HStack(spacing: 0) { | ||||
|                 Text(title) | ||||
|                     .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                     .foregroundColor(Color(hex: "eeeeee")) | ||||
|                  | ||||
|                 Spacer() | ||||
|                  | ||||
|                 Image("ic_forward") | ||||
|                     .resizable() | ||||
|                     .frame(width: 20, height: 20) | ||||
|                     .onTapGesture { | ||||
|                     } | ||||
|             } | ||||
|             .padding(.horizontal, 13.3) | ||||
|              | ||||
|             ContentMainContentThemeView( | ||||
|                 themeList: themeList, | ||||
|                 selectTheme: selectTheme, | ||||
|                 selectedTheme: $selectedTheme | ||||
|             ) | ||||
|              | ||||
|             ScrollView(.horizontal, showsIndicators: false) { | ||||
|                 HStack(spacing: 13.3) { | ||||
|                     ForEach(0..<contentList.count, id: \.self) { index in | ||||
|                         ContentMainItemView(item: contentList[index]) | ||||
|                     } | ||||
|                 } | ||||
|                 .padding(.horizontal, 13.3) | ||||
|             } | ||||
|         } | ||||
|         .onAppear { | ||||
|             selectedTheme = themeList[0] | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     ContentMainNewContentViewV2( | ||||
|         title: "새로운 단편", | ||||
|         onClickMore: {}, | ||||
|         themeList: ["전체", "테스트1", "테스트2"], | ||||
|         contentList: [ | ||||
|             GetAudioContentMainItem( | ||||
|                 contentId: 1, | ||||
|                 coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 title: "ㅓ처랴햐햫햐햐", | ||||
|                 creatorId: 8, | ||||
|                 creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 creatorNickname: "유저1", | ||||
|                 price: 10, | ||||
|                 duration: "00:00:30" | ||||
|             ), | ||||
|             GetAudioContentMainItem( | ||||
|                 contentId: 2, | ||||
|                 coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 title: "ㅓ처랴햐햫햐햐", | ||||
|                 creatorId: 8, | ||||
|                 creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 creatorNickname: "유저2", | ||||
|                 price: 10, | ||||
|                 duration: "00:00:30" | ||||
|             ), | ||||
|             GetAudioContentMainItem( | ||||
|                 contentId: 3, | ||||
|                 coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 title: "ㅓ처랴햐햫햐햐", | ||||
|                 creatorId: 8, | ||||
|                 creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 creatorNickname: "유저3", | ||||
|                 price: 10, | ||||
|                 duration: "00:00:30" | ||||
|             ) | ||||
|         ], | ||||
|         selectTheme: { _ in } | ||||
|     ) | ||||
| } | ||||
| @@ -116,7 +116,7 @@ struct ContentMainViewV2: View { | ||||
|                     case .SERIES: | ||||
|                         ContentMainTabSeriesView() | ||||
|                     case .CONTENT: | ||||
|                         EmptyView() | ||||
|                         ContentMainTabContentView() | ||||
|                     case .ALARM: | ||||
|                         EmptyView() | ||||
|                     case .ASMR: | ||||
|   | ||||
| @@ -0,0 +1,11 @@ | ||||
| // | ||||
| //  GetContentCurationResponse.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2/21/25. | ||||
| // | ||||
|  | ||||
| struct GetContentCurationResponse: Decodable { | ||||
|     let title: String | ||||
|     let items: [GetAudioContentMainItem] | ||||
| } | ||||
| @@ -212,16 +212,10 @@ struct ContentMainTabHomeView: View { | ||||
|                             contentList: viewModel.rankContentList | ||||
|                         ) | ||||
|                         .padding(.top, 30) | ||||
|                         .padding(.horizontal, 13.3) | ||||
|                     } | ||||
|                      | ||||
|                     if viewModel.eventBannerList.count > 0 { | ||||
|                         SectionEventBannerView(items: viewModel.eventBannerList) | ||||
|                             .frame( | ||||
|                                 width: viewModel.eventBannerList.count > 0 ? screenSize().width : 0, | ||||
|                                 height: viewModel.eventBannerList.count > 0 ? screenSize().width * 300 / 1000 : 0, | ||||
|                                 alignment: .center | ||||
|                             ) | ||||
|                             .padding(.top, 30) | ||||
|                     } | ||||
|                      | ||||
| @@ -235,7 +229,6 @@ struct ContentMainTabHomeView: View { | ||||
|                             } | ||||
|                         ) | ||||
|                         .padding(.top, 30) | ||||
|                         .padding(.horizontal, 13.3) | ||||
|                     } | ||||
|                      | ||||
|                     Text(""" | ||||
|   | ||||
| @@ -70,11 +70,6 @@ struct ContentMainTabSeriesView: View { | ||||
|                      | ||||
|                     if !viewModel.eventBannerList.isEmpty { | ||||
|                         SectionEventBannerView(items: viewModel.eventBannerList) | ||||
|                             .frame( | ||||
|                                 width: viewModel.eventBannerList.count > 0 ? screenSize().width : 0, | ||||
|                                 height: viewModel.eventBannerList.count > 0 ? screenSize().width * 300 / 1000 : 0, | ||||
|                                 alignment: .center | ||||
|                             ) | ||||
|                             .padding(.top, 30) | ||||
|                     } | ||||
|                      | ||||
| @@ -88,6 +83,21 @@ struct ContentMainTabSeriesView: View { | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { | ||||
|             HStack { | ||||
|                 Spacer() | ||||
|                 Text(viewModel.errorMessage) | ||||
|                     .padding(.vertical, 13.3) | ||||
|                     .frame(width: screenSize().width - 66.7, alignment: .center) | ||||
|                     .font(.custom(Font.medium.rawValue, size: 12)) | ||||
|                     .background(Color.button) | ||||
|                     .foregroundColor(Color.white) | ||||
|                     .multilineTextAlignment(.leading) | ||||
|                     .cornerRadius(20) | ||||
|                     .padding(.bottom, 66.7) | ||||
|                 Spacer() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -10,13 +10,12 @@ import Kingfisher | ||||
|  | ||||
| struct SectionEventBannerView: View { | ||||
|      | ||||
|     @State private var currentIndex = -1 | ||||
|     @State private var currentIndex = 0 | ||||
|     @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 | ||||
| @@ -26,13 +25,17 @@ struct SectionEventBannerView: View { | ||||
|                             .cancelOnDisappear(true) | ||||
|                             .downsampling( | ||||
|                                 size: CGSize( | ||||
|                                         width: proxy.size.width, | ||||
|                                         height: proxy.size.height | ||||
|                                     width: screenSize().width, | ||||
|                                     height: screenSize().width * 300 / 1000 | ||||
|                                 ) | ||||
|                             ) | ||||
|                             .resizable() | ||||
|                             .scaledToFill() | ||||
|                                 .frame(width: proxy.size.width, height: proxy.size.height, alignment: .center) | ||||
|                             .frame( | ||||
|                                 width: screenSize().width, | ||||
|                                 height: screenSize().width * 300 / 1000, | ||||
|                                 alignment: .center | ||||
|                             ) | ||||
|                             .tag(index) | ||||
|                             .onTapGesture { | ||||
|                                 if let _ = item.detailImageUrl { | ||||
| @@ -46,13 +49,17 @@ struct SectionEventBannerView: View { | ||||
|                             .cancelOnDisappear(true) | ||||
|                             .downsampling( | ||||
|                                 size: CGSize( | ||||
|                                         width: proxy.size.width, | ||||
|                                         height: proxy.size.height | ||||
|                                     width: screenSize().width, | ||||
|                                     height: screenSize().width * 300 / 1000 | ||||
|                                 ) | ||||
|                             ) | ||||
|                             .resizable() | ||||
|                             .scaledToFill() | ||||
|                                 .frame(width: proxy.size.width, height: proxy.size.height, alignment: .center) | ||||
|                             .frame( | ||||
|                                 width: screenSize().width, | ||||
|                                 height: screenSize().width * 300 / 1000, | ||||
|                                 alignment: .center | ||||
|                             ) | ||||
|                             .tag(index) | ||||
|                             .onTapGesture { | ||||
|                                 if let _ = item.detailImageUrl { | ||||
| @@ -66,15 +73,15 @@ struct SectionEventBannerView: View { | ||||
|             } | ||||
|             .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) | ||||
|             .frame( | ||||
|                     width: proxy.size.width, | ||||
|                     height: proxy.size.height, | ||||
|                 width: screenSize().width, | ||||
|                 height: screenSize().width * 300 / 1000, | ||||
|                 alignment: .center | ||||
|             ) | ||||
|              | ||||
|             HStack(spacing: 4) { | ||||
|                 ForEach(0..<items.count, id: \.self) { index in | ||||
|                     Capsule() | ||||
|                             .foregroundColor(index == currentIndex ? Color(hex: "3bb9f1") : Color(hex: "909090")) | ||||
|                         .foregroundColor(index == currentIndex ? .button : .gray90) | ||||
|                         .frame( | ||||
|                             width: index == currentIndex ? 18 : 6, | ||||
|                             height: 6 | ||||
| @@ -101,7 +108,6 @@ struct SectionEventBannerView: View { | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct SectionEventBannerView_Previews: PreviewProvider { | ||||
|   | ||||
| @@ -73,11 +73,6 @@ struct LiveView: View { | ||||
|                              | ||||
|                             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 | ||||
|                                     ) | ||||
|                             } | ||||
|                              | ||||
|                             if viewModel.communityPostItems.count > 0 { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Yu Sung
					Yu Sung